Fork me on GitHub

Python3笔记

文章概述

本篇文章记录学习Python3的笔记。

参考资料

认识python

python代码

  1. Python代码不能压缩和混淆;

命令行操作

命令行信息

1
2
3
4
5
# 获取命令行参数
import sys
print(sys.argv)
# 获取当前Python命令的可执行文件路径
print(sys.executable)

退出控制台

1
2
//windows
$ exit()

语句换行

使用三引号开头表示多行语句,再次使用三引号结束,如下:

1
2
3
4
5
>>> '''
... hello
... world
... '''
'\nhello\nworld\n'

python版本信息

1
$ python --version

输入输出

1
2
3
//input可以不带参数
num=input("请输入一个数字");
print(num);

运行python文件

1
$ python hello.py

编程规范

语句写法注意

  1. 单行语句:不需要加“;”作为结束符;
  2. 代码块:不需要{}包裹,而是使用四个空格(非tab键)缩进包裹,每个语句块结束都要空出一行;
  3. 注释:单行注释使用#,多行注释使用三引号;
1
2
# 单行注释
'''多行注释'''
  1. 变量命名:以小写字母开头,可以使用驼峰命名或者下划线分割字母的方式命名;
  2. 函数命名:函数名使用小写字母,单词之间用_下划线分割;
  3. 语句换行:
1
2
3
4
5
6
7
8
9
# 方式一:末尾加\:换行【不推荐是使用】
from xlib.firstlib import a, b,\
c
print('end')

# 方式二:圆括号包裹换行【推荐使用】
from xlib.firstlib import (a, b,
c)
print('end')

变量

变量

python是动态语言,定义变量直接对变量赋值,而无需指定变量类型;

1
2
y = 10
x = y
变量命名

变量命名:以小写字母开头,可以使用驼峰命名或者下划线分割字母的方式命名;

变量的类型
  • 值类型(不可改变):int、str、tuple
  • 引用另类型(引用类型):list、set、dict
变量内存地址

使用id(var)获取变量var的内存地址:

1
2
3
>>> x = 10
>>> id(x)
140707502150976

常量

python中并未严格定义常量的表达式,只是推荐用大写字母来表示常量;

1
PI = 3.14159265359

运算符

算数运算符

1
2
+、-、*、/、//(整除)、%
**(次方):2**3=8

赋值运算符

1
=,+=,*=,/=,%=,**=(幂赋值运算符),//=

关系运算符

比较值是否相等,返回布尔值;

1
==,!=,>,<,>=,<=

逻辑运算

and、or和not来表示计算机的与、或和非运算;

成员运算符

  • in:1 in [1,2]=true (注意:字典的成员运算只针对key)
  • not in : 与in相反;

身份运算符

返回布尔值

  • is :比较的是内存地址是否相等;
  • is not:与is相反;

位运算符

位运算符先转换成2进制进行运算结果再转换回来;

1
2
3
4
5
6
&:1&1=1,1&0=0,0&0=0;
|
^(按位异或)
~
<<
>>

位运算符还可以用于集合之间的运算:

  • 求集合交集:{1,2}&{1}={1}
  • 求集合合集:{1,2,3,4}|{3,4,7}={1,2,3,4,7}

运算符优先级

运算符优先级从最高到最低如下表:

运算符 描述
** 指数运算 (最高优先级),如:2**3=8
~、+、- 按位翻转, 一元加号和减号 (最后两个的方法名为 +@ 和 -@)
*、/、%、// 乘,除,取模和取整除
+、- 加法减法
>>、<< 左移、右移位运算符
&、 按位与
^、丨 运算符
<=、<、>、>= 比较运算符
<>、==、!= 比较运算符
=、%=、/=、//=、-=、+=、*=、**= 赋值运算符
is、is not 身份运算符
in、not in 成员运算符
and、or、not 逻辑运算符(and的优先级大于or)

数据类型

数据类型概览

  1. 基础数据类型有:
  • 整数(int):十六进制0x44表示整数更方便,Python的整数没有大小限制;
  • 浮点数(float):e代替10,如1.23e9,Python的浮点数没有大小限制,但是超出一定范围就用inf表示无限大;
  • 字符串(str);
  • 布尔值(bool):True(非0的数值或非空的类型) False(0或空值或None) ,可以使用or、and、not(或与非)运算表达式表示;
  • None(NoneType):空值;
  • bytes: 字节,用带b前缀的单引号或双引号字符串表示,如:b’abc’;
  • complex(复数):python复数单位用j表示,如36j(a+bi(a,b均为实数)的数称为复数,其中a称为实部,b称为虚部,i称为虚数单位);

2. 全部数据类型概览图

image

思维导图中的数据类型有以下特点:

  1. 不可变的类型有:数字、字符串、元组;
  2. 有序的的类型有:字符串、列表、元组,有序的数据类型可以用索引(索引是从0开始)访问、可以对序列进行截取的切片操作;
  3. 无序的数据类型:集合、字典,没有索引,不可以通过切片操作来截取;

数据类型相关方法

获取数据的类型
1
2
>>> type(0);
<class 'int'>
判断数据类型

判断数据类型的正确方式:isinstance(var_name, data_type),该方法返回bool值;

1
2
>>> isinstance(1,int)
True
数据类型相互转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# str/float->int
int('1') # 1
int(0.01) # 0

# 将进制数转换为int类型的10进制数:int(str_x,base=进制)
int('1001',2) # 9
int('1001',base=2) # 9

# int/list/tuple->str
str(1) # '1'
str([1,2]) # '[1,2]'

# tuple->list
list((1,2,3)) # [1,2,3]

# str->bytes
bytes('hello', encoding='utf-8')
'str'.encode('utf-8')

# bytes->str
b=b'\xe9\x80\x86\xe7\x81haha\xab'
b.decode('utf-8',errors='ignore') # 忽略非法字符,用strict会抛出异常

#...以此类推其它类型转换

数字

数字包括整数、浮点数、负数、进制数…

进制数
常见进制数
  • 二进制:0b作前缀;
  • 八进制:0o作前缀;
  • 十六进制:0x作前缀;
进制转换方法
  • 任意进制数转二进制:bin(任意进制数)
1
2
3
10->2:bin(10)
8->2:bin(0o1)
16->2:bin(0xE)
  • 任意进制数转十进制:int(任意进制数)
  • 任意进制数转八进制:oct(任意进制数)
  • 任意进制数转十六进制:hex(任意进制数)
数学运算
保留小数

round(number[,n]):number四舍五入保留n位小数;

1
2
//示例:
round(3.2132,2)

布尔类型

type = bool

非零数值、非空字符串、非空list等,就判断为True,否则为False。

  1. True、False表示布尔值(请注意大小写);
  2. 布尔值可以用and(与)、or(或)和not(非)运算来表示;
  3. 布尔值可以用条件运算的结果来表示;

空值

type = None

  1. 空值用None表示。
  2. None不能理解为0,因为0是有意义的,而None是一个特殊的空值。
  3. 判空操作:
1
2
3
4
5
6
//非空判断
if a:
pass
//空判断
if not a:
pass

字符串

type = str

编码
  • Python 3的字符串使用Unicode,直接支持多语言。
UTF-8
  • UTF-8编码是可变长编码,UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节,对比Unicode,更加节省空间;
  • 1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节
Python文件设置编码

当Python解释器读取源代码时,为了让它按UTF-8编码读取,我们通常在文件开头写上这两行:

1
2
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
  • 第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序,Windows系统会忽略这个注释;
  • 第二行注释是为了告诉Python解释器,按照UTF-8编码读取源代码,否则,你在源代码中写的中文输出可能会有乱码
转义字符

常用\表示转义字符,常用方式:

  • 换行符:\n
  • 横向制表符:\t
  • 回车:\r
格式化字符串

格式化字符串的方法:

  1. 在字符串中使用%占位符,字符串后跟%,%后跟需要被替换的字符串;
  2. 使用字符串的format方法;
占位符%格式化字符串
  1. 动态字符串可以使用%占位符的方式格式化字符串,常见占位符如下:
占位符 替换内容
%d 整数
%f 浮点数
%s 字符串
%x 十六进制整数
  1. 格式化整数和浮点数还可以指定是否补0和整数与小数的位数:

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
//替换单个字符串
>>> 'Hello, %s' % 'world'
'Hello, world'
//多个替换
>>> 'Hi, %s, you have $%d.' % ('Michael', 1000000)
'Hi, Michael, you have $1000000.'

//保留2位整数,区分是否补0的情况:
>>> print('%2d-%02d' % (3, 1))
3-01
//保留两位小数:
>>> print('%.2f' % 3.1415926)
3.14
format方法格式化字符串

另一种格式化字符串的方法是使用字符串的format()方法,它会用传入的参数依次替换字符串内的占位符{0}、{1}……,不过这种方式写起来比%要麻烦得多:

1
2
>>> 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125)
'Hello, 小明, 成绩提升了 17.1%'
字符串操作方法
长度

len(str);

len()函数实际上是调用str对象的len()方法来获取字符串的长度,二者等价;

1
2
3
4
>>> len('ABC')
3
>>> 'ABC'.__len__()
3
乘法复制

字符串乘法(字符串复制):重复n次输出乘号前面的字符串;

1
2
3
// [示例]
>>> "hello"*3;
'hellohellohello'
索引取值

如果下标越界会报IndexError

1
2
3
4
5
6
//从左到右:
>>> 'hello'[0];
'h'
//从右到左:
>>> 'hello'[-2];
'l'
索引切片(截取)

字符串按索引截取:str[a:b:step],截取str的a到b索引的元素,不包含b,步长为step;

  • str[star,end]切片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//按索引截取
>>> "hello"[1:2];
'e'
//[6:]:从第n个截取到最后;
>>> "hello"[0:];
'hello'
//[6:0]:一个都不截取;
>>> "hello"[5:];
''
//从索引为0的元素截取到整个字符串倒数第2个元素:
>>> "hello"[:-2];
'hel'
//从倒数第2个元素截取到最后:
>>> "hello"[-2:];
'lo'
  • str[star,end,step]切片
1
"hello world"[0:8:2] # 从索引为0截取到索引为7的位置,步长为2;
字符索引

获取字符串在另一个字符串中的索引:

1
2
>>> 'hello'.index('o');
4
字符串替换

replace方法会替换字符串中所有符合条件的字符串或字符;

1
2
>>> 'hello'.replace('lo','*');
'hel*'
去除左右空格
1
2
>>> '   hello  '.strip();
'hello'
字符串转换
1
2
3
4
5
6
7
8
9
10
11
12
//获取字符的整数编码:
ord(str);
//把整数编码转换为对应的字符:
chr(code);
//字符串转bytes:
'ABC'.encode('ascii');
'中文'.encode('utf-8');
//bytes转字符串:
b'ABC'.decode('ascii'); //'ABC'
b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8') //'中文'
//bytes解码,使用errors='ignore'忽略错误的字节
b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore') //'中'

list

type = list

list(列表)是一个可变的有序表,可以添加或删除其中的元素。

列表的定义

列表类似js中的数组,支持任意数据类型的数据组合,支持一维到多维list;

1
2
3
4
//一维:任意类型的组合,通过下标索引来访问;
arr1 = [1,2,3,4,'hello',True];
//多维列表:实质就是嵌套列表,通过arr[1][2]这种形式访问;
[[1,2],[3,4,5],[True,False],'hello']
列表的操作

列表的操作与有序数据类型的操作类似,请参考字符串的操作

CRUD操作

list=[]列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//添加元素
list.append('Adam')
//添加元素到指定索引位置
list.insert(1, 'Jack')
//删除末尾元素
list.pop()
//删除指定索引位置的元素
list.pop(1)
//指定位置元素重新赋值
list[1] = 'Sarah'
//获取最后一个元素
list[-1]
//获取指定索引的值
list[0]
列表长度
1
len(list)
数学运算
  • 列表的加法:[1,2]+[3]=[1,2,3]
  • 列表的乘法:[1,2]*2=[1,2,1,2]
截取

支持类似字符串的冒号(:)切片操作,截取指定索引的列表元素(请参考字符串的操作);

函数生成列表

使用list(range(startIndex,stopIndex,stepLength))表达式生成list:

1
list(range(1, 22))
列表生成式

列表生成式指在[]内使用一行表达式生成一个列表list的表达式;

列表生成式作用的目标可以是列表、集合、元组,最终结果是list;

1.列表生成式作用于list:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 循环列表生成式
print([x * x for x in range(1, 11)])

# os.listdir可以列出当前文件夹下的文件和目录
import os
print([d for d in os.listdir('.')])

# 多层循环列表生成式
print([m + n for m in 'ABC' for n in 'XYZ'])
print([m + n + str(x) for m in 'ABC' for n in 'XYZ' for x in range(1, 4)])

# 带判断的列表生成式
print([x * x for x in range(1, 11) if x % 2 == 0])

# 列表生成式遍历dict字典内容
d = {'x': 'A', 'y': 'B', 'z': 'C'}
print([k + '=' + v for k, v in d.items()])

2.列表生成式作用在元组:

1
2
3
4
a = (1, 2, 3)
r = [i ** i for i in a]
print(r)
# [1,4,9]

元组(tuple)

type = tuple

元组:tuple。tuple和list非常类似,都是Python内置的有序集合,但是tuple不可变,一旦初始化就不能修改,没有添加和删除方法,所以代码更安全,如果可能,能用tuple代替list就尽量用tuple;

注意:tuple如果内含可变元素,可变元素内部可以改变;

元组定义

tuple的元素定义在圆括号()里;

1
2
3
4
5
6
//定义一个元组:
tupleA = ('Michael', 'Bob', 'Tracy',1,22)
//定义一个空的tuple:
t = ();
//定义只有1个元素的tuple:
t = (1,)
函数生成元组

使用tuple(range(startIndex,stopIndex,stepLength))方法直接生成一个tuple:

1
tuple(range(1, 22))

序列

有顺序,每个元素都分配一个序号,序列包括:列表、元组、str,他们之间的操作都类似;

序列操作
切片(截取)

支持切片(截取)操作,参考字符串切片操作;

逻辑包含

支持逻辑in、not in运算符:

1
2
2 in [1,2] # True
2 not in [1,2] # False
序列内置函数
最大值
1
max(序列)
最小值
1
min(序列)
长度
1
len(序列)
join

join函数使用分隔符将序列类型的变量组合成一个字符串;

1
2
3
4
s = ','.join('hello')
print(s)
# h,e,l,l,o
# <class 'str'>

集合

type = set

集合的定义
  • 使用花括号{}包含元素,元素是不可变类型数据,如字符串、数字和元组;
  • set是一组元素的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key
定义集合变量
  • 方式一:直接赋值
1
2
3
>>> x={1,2,3,"hello"}
>>> type(x)
<class 'set'>
  • 方式二:通过set([…])定义,需要提供一个list作为输入集合;
1
2
3
>>> x=set({1})
>>> x
{1}
空集合

空集合:set(),注意{}不能表示空集合,type({})=dict(字典)

1
2
>>> type(set())
<class 'set'>
集合的操作
添加

通过add(key)方法可以添加元素到set中,可以重复添加,但不会有效果;

1
2
3
4
>>> x = {1}
>>> x.add(4)
>>> x
{1, 4}
删除

通过remove(key)方法可以删除元素;

1
2
3
4
>>> s = {1,2,3}
>>> s.remove(1)
>>> s
{2, 3}
集合的运算
  • 支持逻辑包含:in、not int;
  • 支持求集合差集:{1,2,3}-{1}={2,3}
  • 支持求集合交集:{1,2}&{1}={1}
  • 支持求集合合集:{1,2,3,4}|{3,4,7}={1,2,3,4,7}

字典

type = dict

Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。

特点
  • 查找和插入的速度极快,不会随着key的增加而变慢;
  • 需要占用大量的内存,内存浪费多。
  • 无序的(不支持索引)、不支持切片操作、不重复。
字典的定义

字典的定义类似java的map,用花括号包裹许多个key:value,用逗号分隔;

语法格式:{key1:value1,key2:value2,…}

注意:key是不可变类型,value可以是任意类型;

空字典

空的字典:type({})=dict

示例
  • 例1:
1
2
3
>>> d = {1:'one','two':2}
>>> d
{1: 'one', 'two': 2}
  • 例2:
1
2
3
>>> x = {1:2,'1':1,(1,3):1,3:{1,3},6:{'one':'hello'}}
>>> x
{1: 2, '1': 1, (1, 3): 1, 3: {1, 3}, 6: {'one': 'hello'}}
key的特点
  • key不能相同,相同则会覆盖对用的value值;
  • key中数字和字符串被认为不同的key,如1,“key”;
1
2
3
>>> x = {1:2,'1':1}
>>> x
{1: 2, '1': 1}
  • key的数据类型:是不可变的类型,包括:int,str,元组;
1
2
3
4
5
>>> x = {1:2,'1':1,(1,3):1}
>>> x
{1: 2, '1': 1, (1, 3): 1}
>>> x.get((1,3))
1
字典的操作
添加
  • 直接通过d[key]=value的方式赋值;
1
2
3
4
>>> d = {}
>>> d[1]='one'
>>> d
{1: 'one'}
  • 通过内置函数setdefault(key,defaultValue):如果键不存在于字典中,将会添加键并将值设为defaultValue;
1
2
3
4
5
>>> d={}
>>> d.setdefault(1,'one')
'one'
>>> d
{1: 'one'}
根据key获取值
  • d[key]: 中括号索引的方式,如果key不存在,dict就会报错;
1
2
3
>>> d = {1:'one'}
>>> d[1]
'one'
  • d.get(key)的方式:如果key不存在,可以返回None;
1
2
3
4
5
6
7
8
9
10
>>> d = {1:'one'}
>>> d[1]
'one'
>>> d.get(1)
'one'

>>> d ={}
>>> d.get(1)
>>> type(d.get(1))
<class 'NoneType'>
  • d.get(key,default)的方式:如果key不存在返回default,但不会新增到字典中;
1
2
3
4
5
6
7
>>> d = {}
>>> d
{}
>>> d.get(1,'one')
'one'
>>> d
{}
删除key

要删除一个key,用pop(key)方法,对应的value也会从dict中删除:

1
2
3
4
5
>>> d = {1:'one'}
>>> d.pop(1)
'one'
>>> d
{}
遍历字典

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 直接遍历字典,默认遍历结果是dict的key
students = {'name': '张三'}
for key in students:
print(key)

# 遍历返回key_value为key和value组成的元组
for key_value in students.items():
print(key_value)

# 遍历key:
for key in students.keys():
print(key)

# 遍历value:
for value in students.values():
print(value)

示例:

1
2
3
4
d = {1: 'one', 2: 'two'}
for key_value in d.items():
print(key_value)
# 结果:(1, 'one') (2, 'two')

数据解构赋值

序列解包类似js里的解构赋值,但是解构左右两边数量必须对等;

生成器

一边循环一边计算,根据前面的元素推算后面的元素的机制,称为生成器generator,generator也是可迭代对象;

表达式生成器

构建一个generator只需要将列表生成式的[]换成()即可得到generator对象;

1
2
3
4
5
6
7
8
L = [x * x for x in range(3)]
g = (x * x for x in range(3))
# print(g)
print(next(g))
print(next(g))
print(next(g))
# 溢出时报错: StopIteration
print(next(g))
函数生成器
  • 如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator;
  • yield关键字:在每次调用next()的时候,执行函数遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。
定义示例

以实现一个斐波那契数列函数为例来实现一个generator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 原实现斐波那契数列的函数实现
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'

fib(3)

# yield关键字实现generator:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'


f = fib(6)
函数返回值

如果定义函数生成器最终return返回一个值,如果需要获得返回值的话,必须捕获StopIteration错误,返回值包含在StopIteration的value中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'

f = fib(6)
while True:
try:
x = next(f)
print('g:', x)
except StopIteration as e:
print('Generator return value:', e.value)
break
生成器迭代

使用next(g)可以遍历generator,但是不知道临界,所以一般都使用循环来迭代generator(不需要关心StopIteration的错误):

1
2
3
4
# 循环来遍历生成器
g = (x * x for x in range(3))
for n in g:
print(n)

迭代器

Iterable

可以直接作用于for循环的对象统称为可迭代对象:Iterable

可迭代的数据类型有:

  • 一类是集合数据类型,如list、tuple、dict、set、str等;
  • 一类是generator,包括生成器和带yield的generator function。
Iterable对象判断

可以使用isinstance(aim,Iterable)内置函数来判断是否是Iterable对象:

1
2
3
4
5
6
7
8
9
10
11
>>> from collections import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False
Iterator
  • 可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。
  • Python的for循环本质上就是通过不断调用next()函数实现的;
Iterator对象判断

可以使用isinstance()判断一个对象是否是Iterator对象:

1
2
3
4
5
6
7
8
9
>>> from collections import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False
Iterable转Iterator

生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。

把list、dict、str等Iterable变成Iterator可以使用iter()函数:

1
2
3
4
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True
Iterator数据流对象
  • Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
  • Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

条件控制语句

条件判断

if条件判断语句,判断表达式必须,返回结果可以使用pass占位,pass是空语句/占位语句;

语法
1
2
3
4
5
6
7
8
if <条件判断1>:
<执行1>
elif <条件判断2>:
<执行2>
elif <条件判断3>:
<执行3>
else:
<执行4>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# if
if expression:
pass

# if...else
if condition:
pass
else:
pass

# if...elif..else
if condition:
pass
elif expression:
pass
else:
pass
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
# eg1:
MOOD=False
if mood :
print('good mood')
else :
print('bad mood')

# eg2:
CONDIITON = False
if CONDIITON:
print('True')
elif not CONDIITON:
print('False')

三元表达式

python中的三元表达式的效果类似java中的三目运算符(expression?r2:r2);

  1. 表达式格式:结果为真的返回 if 表达式 else 结果为假的返回
  2. 示例:
1
2
3
4
5
# python中的三元表达式
x = 2
y = 3
r = x if x > y else y
print(r)

循环

while循环
语法

当while表达式满足会一直循环执行while语句块中的逻辑,当不满足时跳出循环,或者执行else语句块;

1
2
3
4
5
6
7
8
9
# while
while expression:
pass

# while...else...
while expression:
pass
else:
pass
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
# eg1
counter = 1
while counter<=10:
print(counter)
counter+=1

# eg2
counter=0
while counter<=10:
counter+=1
print(counter)
else:
print('结束')
for循环
  • for循环的目标是可迭代对象类型(Iterable)的数据,就可以使用for循环进行迭代。
  • 判断一个对象是否是可迭代对象,通过collections模块的Iterable类型判断;
1
2
3
4
5
6
7
>>> from collections import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False
for…in…循环
1
2
3
4
# for...in...循环遍历列表
arrays=['apple','orange','banana']
for fruit in arrays:
print(fruit+',', end='')
1
2
3
4
5
# 嵌套for...in...循环遍历列表;
arrays=[['apple','orange','banana'],(1,3,4)]
for x in arrays:
for y in x:
print(y)
for…in…else循环
1
2
3
4
5
6
7
# for...in...else循环结束后执行else;
arrays=[['apple','orange','banana'],(1,3,4)]
for x in arrays:
for y in x:
print(y)
else:
print('game over')
for…in range循环

for…in range(startIndex,endIndex,step)循环按索引访问数据,不包含endIndex;

1
2
3
4
5
6
7
8
9
# 以步长1,遍历列表:
arrays = [1,2,3,4,5,6,7,8]
for i in range(0,len(arrays),1):
print(arrays[i], end=' | ')

# 以步长1,倒序遍历列表:倒序时步长要为负数;
arrays = [1,2,3,4,5,6,7,8]
for i in range(len(arrays)-1,0,-1):
print(arrays[i], end=' | ')
for循环获取索引

Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:

  • eg1: 遍历列表:
1
2
3
4
5
6
>>> for i, value in enumerate(['A', 'B', 'C']):
... print(i, value)
...
0 A
1 B
2 C
  • eg2: 遍历由元组组成的列表:
1
2
3
4
5
6
7
for i, (x, y) in enumerate([(1, 1), (2, 4), (3, 9)]):
print(i, x, y)

# 结果
# 0 1 1
# 1 2 4
# 2 3 9

循环控制语句

break

break 仅终止当前循环,终止当前循环后不会执行当前循环的else,如果外层还有循环,则继续执行外层的循环;

1
2
3
4
5
6
7
arrays=[1,3,4]
for x in arrays:
if x == 3:
break
print(x)
else:
print('break终端循环后,不会执行else的逻辑')
continue

continue:结束当前循环,继续执行下个循环;

1
2
3
4
5
6
7
arrays=[1,3,4]
for x in arrays:
if x == 3:
continue
print(x)
else:
print('结束')

Python实现java语言switch特性

Python中没有类似java语言的switch API,可以用elif或者字典来代替;

字典switch

这里主要是dict字典来实现switch语法;

  • key对应简单的值:
1
2
3
4
5
6
7
# 简单的switch,利用字典的key实现
swicher = {
1:'one',
2:'two',
3:'three'
}
print(swicher.get(7,'default'))
  • key对应函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def getOne(*args):
return 'one'

def getTwo(*args):
return 'two'

def getThree(*args):
return 'three'

def getDefault(*args):
return 'invalid value='+str(args[0])

swicher = {
1 : getOne,
2 : getTwo,
3 : getThree
}

day = 12
# 函数中传参数,在最后的()中传:
day_name = swicher.get(day,getDefault)(day)
print(day_name)

函数

内置函数

内置函数文档地址

python内置了许多函数,可以直接调用,常用的内置函数有:

  • abs(x): 计算x的绝对值;
  • max(a,b,…): 获取多个参数的最大值;

函数命名

函数命名:函数名使用小写字母,单词之间使用下划线(_)分割;

定义函数

  • 定义一个函数要使用def语句,依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回。
  • 如果没有return语句,函数执行完毕后也会返回结果,只是结果为None。
  • return None也可以简写为return。
py文件定义函数
  • 定义一个空函数:
1
2
def funcname(parameter_list):
pass
  • 定义一个求绝对值的函数;
1
2
3
4
5
6
7
8
def my_abs(x):
if x >= 0:
return x
else:
return -x

# 调用函数
print(my_abs(-5)) # 5
python交互环境定义函数

在Python交互环境中定义函数时,注意Python会出现…的提示。函数定义结束后需要按两次回车重新回到>>>提示符下:

1
2
3
4
5
6
7
8
>>> def my_abs(x):     
... if x >= 0:
... return x
... else:
... return -x
...
>>> my_abs(-9)
9
函数返回多个值

函数返回多个值时,python将多个值放到一个tuple中,语法上可以省略():

1
2
3
4
def multi_value():
return 1, 88

print(multi_value()) # (1, 88)
函数的参数
默认参数

定义函数时,函数的参数可以设置默认值,设置默认值使用等号直接赋值即可:

1
2
3
4
def myfn(name, sex='man'):
return 'name=' + name + ',sex=' + sex

print(myfn('ethan'))

定义默认参数要牢记一点:默认参数必须指向不变对象!

可变参数

可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple.

定义函数时接收的参数个数不确定时,可以在参数前加*,此时参数就是可变参数;

在调用接收可变参数的函数时,如果传入已有的list或tuple作为多个参数时,需要在调用处给list或tuple前也加*,这样list或tuple的元素才能正确转化为一个元组,否则转化后的元组只有整个list或整个tuple作为1个参数;

1
2
3
4
5
6
7
8
9
10
11
def myfn(*args):
print(type(args))
print(args)
return

print(myfn(*[1, 33]))

# 结果:
# <class 'tuple'>
# ('ethan', 1, 33)
# None
关键字参数

关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict.

关键字参数使用 **kw表示;

在调用接收关键字参数的函数时,如果传入已有的dict作为多个参数时,需要在调用处给dict前也加**,这样dict的元素才能正确转化为一个dict,否则转化后的dict只有整个dict作为1个参数;

1
2
3
4
5
6
7
8
9
10
11
def myfn(**kw):
print(type(kw))
print(kw)
return

d = {'name': 'ethan'}
print(myfn(**d))
# 结果:
# <class 'dict'>
# {'name': 'ethan'}
# None
命名关键字参数

命名关键字参数主要是为了限制调用者传入哪些参数名的参数,如果传入了函数未声明接收的参数名则会报错,是非必要参数;

注意点

1
2
3
4
5
- 关键字参数**kw不同,命名关键字参数需要一个特殊分隔符*,*后面的参数被视为命名关键字参数。
- 如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了。
- 命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错。
- 命名关键字参数可以有缺省值,即可以设置默认值;
- 使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*作为特殊分隔符。如果缺少*,Python解释器将无法识别位置参数和命名关键字参数。

示例

1
2
3
4
5
6
7
def myfn(name, *, city, age=23):
print(name + "," + str(age) + "岁,现居" + city)
return

d = {'city': '北京'}
myfn('ethan', **d)
# 结果:ethan,23岁,现居北京
参数组合

在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。

参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

要点

1
2
-  虽然可以组合多达5种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。
- 对于任意函数,都可以通过类似func(*args, **kw)的形式定义。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
def f1(a, b, c='None', *args, name, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'name =', name, 'kw =', kw)

def f2(a, b, c='None', *, name, **kw):
print('a =', a, 'b =', b, 'c =', c, 'name =', name, 'kw =', kw)

d1 = {'name': 'name'}
d2 = {'other': 'other'}

f1('a', 'b', 'c', '*args', **d1, **d2)
# a = a b = b c = c args = ('*args',) name = name kw = {'other': 'other'}
f2('a', 'b', 'c', **d1, **d2)
# a = a b = b c = c name = name kw = {'other': 'other'}

函数的要点

  • 函数可以赋值给变量:f=fn()->f();
  • 函数可以作为参数:fn(fn1());
  • 函数内可以再定义函数,函数可以作为函数的返回;
  • 函数内的变量和模块的变量同名时,就近原则,优先使用函数内定义的变量;
  • 函数内给模块变量赋值:python就会看做这是重新定义一个与模块变量同名的局部变量;
  • 函数内给模块变量赋值:需要在函数内使用global关键字重新声明一下模块变量为全局变量;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 函数中的函数调用
def curve_pre():
a=25
def curve(x):
return a*x*x
return curve

f = curve_pre()
print(f(2))

# 函数中重新赋值模块变量:
origin=0
def count_step(step):
global origin
new_step = origin + step
origin = new_step
return new_step

print(count_step(2))
print(count_step(3))

递归函数

如果一个函数在内部调用自身本身,这个函数就是递归函数。

1
2
3
4
5
6
def fact(n):
if n == 1:
return 1
return n * fact(n - 1)

print(fact(998))
递归栈溢出

使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试fact(1000);

解决递归调用栈溢出的方法是通过尾递归优化;

尾递归优化

尾递归是指,对函数返回的优化,即:函数返回时调用函数自身,并且,return语句不能包含调用函数自身之外的表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。

严重警告:Python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。

函数式编程

纯粹的函数式编程语言编写的函数没有变量;

Python的函数允许使用变量,因此,Python不是纯函数式编程语言。

函数式编程特点

  • 允许使用变量;
  • 允许把函数本身作为参数传入另一个函数;
  • 函数允许返回一个函数;
  • 函数内可以定义函数;
  • 函数名可以赋值给变量,此时不会立刻执行函数,使用变量名加圆括号f()调用;

闭包

当函数返回一个函数名时,函数名对应的函数并没有立刻执行,而是直到调用了f()才执行,这称为闭包(closure).

闭包的特点
  • 闭包返回的函数并未执行,返回函数中不要引用任何可能会变化的变量;
  • 闭包返回的函数每次调用都会返回一个新的函数,并且每次调用互不影响;
  • 闭包返回的函数内,通过nonlocal关键字声明变量不是局部变量,而是父函数的变量;
示例
  • eg1: 一个简单的闭包
1
2
3
4
5
6
7
8
9
10
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs

f= count()
f() # 9
  • eg3:闭包返回的函数内的非局部变量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
origin = 0

def factory(pos):
x = pos

def go(step):
# nonlocal声明非局部变量,即父函数的变量;
nonlocal x
new_step = step + x
x = new_step
return new_step

return go

tourist = factory(origin)
print(tourist(2)) # 2
print(tourist(3)) # 5
闭包函数的闭包信息
  • 打印f函数的闭包信息
1
print(f.__closure__)
  • 打印f函数闭包内环境变量(环境变量按字母排序对应索引)的值
1
print(f.__closure__[0].cell_contents)
闭包的缺点
  • 闭包根函数的变量(环境变量)常驻内存,容易发生内存泄漏;

匿名函数

在Python中,对匿名函数提供了有限支持,匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。

语法

关键字lambda用来修饰匿名函数,使用冒号分割参数和返回表达式,冒号前面的x表示函数参数,冒号后面是返回结果。

1
2
3
4
5
6
# 匿名函数示例
lambda x: x * x

# 等价于
def f(x):
return x * x

装饰器

在代码运行期间动态增加函数的功能而且不会改变原有函数的定义的方式,称之为装饰器(Decorator)。

装饰器:可以不改变函数逻辑,给函数新增功能;

装饰器函数
  1. 定义装饰器函数:
    定义一个装饰器函数,需要接收一个被装饰函数为参数,装饰器内部定义一个返回被装饰函数的扩展功能函数,最终装饰器函数返回扩展功能函数 (需要注意:扩展函数应该加@functools.wraps(func)修饰,确保最终返回的函数保持被装饰函数本身的属性)
  2. 使用装饰器函数:
    当我们给被装饰函数上方添加声明:@装饰器函数名,此时直接调用被装饰函数时就得到了扩展的功能,实际是一种装饰模式(AOP编程思想);
示例
  1. 定义一个打印日志的装饰器函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import functools

def log(func):
# @functools.wraps的作用是让返回的wrapper函数保持func函数的属性;
@functools.wraps(func)
def wrapper(*args, **kw):
# 在运行被修饰的函数前打印一行日志
print('call %s() start' % func.__name__)
return func(*args, **kw)

return wrapper

# @log = log(func_test)
@log
def func_test():
print("hello")
# 调用结束的log
print('call %s() end' % 'func_test')

f = func_test
f()
print(f.__name__)
装饰器函数带参数

如果装饰器本身需要传入参数,定义最外层装饰器函数接收参数,内部定义一个无参数的装饰器函数,最外层有参数的装饰器函数返回无参数的装饰器函数名即可;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import functools

def log_has_argument(text):
def decorator(func):
# @functools.wraps的作用是让返回的wrapper函数保持func函数的属性;
@functools.wraps(func)
def wrapper(*args, **kw):
print('接收了一个参数text=%s' % text)
return func(*args, **kw)

return wrapper
return decorator

# @log = log(args)(func_test)
@log_has_argument('我是参数')
def func_test():
print("hello")
# 调用结束的log
print('call func_test() end')

f = func_test
f()
装饰器通用优化
  • 装饰器内给被装饰函数支持可变参数:func(*args);
  • 装饰器内给被装饰函数支持可变参数和可变关键字参数:func(args,*kw);

偏函数

Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。

创建偏函数
1
2
3
4
1> 创建偏函: functools.partial就是帮助我们创建一个偏函数的;
2> 创建偏函数时,实际上可以接收函数对象、*args和**kw这3个参数;
3> functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值)。
4> 调用创建好的偏函数,还可以改变默认参数;
示例
  1. 创建一个偏函数:用于求2进制数对应10进制:
1
2
3
4
5
6
7
import functools

# 创建一个偏函数:用于求2进制数对应10进制
int2 = functools.partial(int, base=2)
print(int2('1000000'))
# 调用创建好的偏函数,可以改变默认参数
print(int2('1000000', base=10))
应用场景

当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数(偏函数),这个新函数可以固定住原函数的部分参数,从而在调用时更简单。

内建的高阶函数

变量可以指向函数,函数的参数能接收变量,函数可以接收另一个函数作为参数,这种函数就称之为高阶函数。

python提供了许多内建的高阶函数供开发者调用;

map

map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。

语法

1> 语法格式:

1
map(fn,iterableData)

2> 参数说明:

  • fn:函数;
  • iterableData:iterable类型的数据;
示例
  1. 对序列中的每个元素求平方:
1
2
3
4
5
6
def f(x):
return x * x

r = map(f, [1, 2])
print(list(r))
# [1,4]
  1. 将序列的元素都转str:
1
2
>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']
reduce

reduce函数来自functools模块;

reduce把一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,返回一个计算结果,其效果就是:

1
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
语法
1
reduce(function, sequence, initial=None)
示例
  1. reduce累加序列中元素的实现:
1
2
3
4
5
6
from functools import reduce

def fn(x, y):
return x + y

print(reduce(fn, [1, 3, 5]))
  1. 把序列[1, 3, 5, 7, 9]变换成整数13579:
1
2
3
4
5
6
from functools import reduce

def fn(x, y):
return x*10 + y

print(reduce(fn, [1, 3, 5]))
filter

Python内建的filter()函数用于过滤序列,filter()函数返回的是一个Iterator;

和map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。

示例
  1. 在一个list中,删掉偶数,只保留奇数:
1
2
3
4
5
6
def is_odd(n):
return n % 2 == 1

r = list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
print(r)
# [1, 5, 9, 15]
sorted
  • Python内置的sorted()函数就可以直接对list进行排序;
1
2
>>> sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]
  • sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序;
语法
1
2
sorted(list, func, reverse):
# reverse: 是否倒序;
示例
  1. 对list元素求绝对值后再排序:
1
2
>>> sorted([36, 5, -12, 9, -21], key=abs, reverse=false)
[5, 9, -12, -21, 36]
注意
  • 默认情况下,对字符串排序,是按照ASCII的大小比较的,由于’Z’ < ‘a’,结果,大写字母Z会排在小写字母a的前面。

模块

模块的概念

在Python中,一个.py文件就称之为一个模块(Module)。

  • 模块是一组Python代码的集合,可以使用其他模块,也可以被其他模块使用。
  • 模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中。
模块的命名
  • 模块名要遵循Python变量命名规范,不要使用中文、特殊字符;
  • 模块名不要和系统模块名冲突,最好先查看系统是否已存在该模块,检查方法是在Python交互环境执行import abc,若成功则说明系统存在此模块。

为了避免模块名冲突,Python引入了按目录来组织模块的方法,称为包(Package)。

包的定义
1
如果目录文件夹中有一个__init__.py文件,Python就把这个目录当成包,否则Python就把这个目录当成普通目录。
要点
1
2
1> __init__.py可以是空文件,也可以有Python代码,因为__init__.py本身就是一个模块,它的模块名就是包名。
2> 包可以有多级目录,即包内可以包含包,组成多级层次的包结构。
示例
  1. mypackage多级层次的包结构:
1
2
3
4
5
6
7
8
9
mypackage
├─ __init__.py
├─ web
│ ├─ __init__.py
│ ├─ utils.py
│ └─ www.py
├─ __init__.py
├─ abc.py
└─ xyz.py

导入模块

Python模块文件导入另一个模块后,当前模块就可以通过(包名.)模块名或者别名来访问模块中的所有功能了。

导入模块注意点
  • 当python中的模块1导入了模块2,则模块2里的代码会被执行一遍;
  • 避免循环导入:如module1引用module2,module2又引用module1;
语法

导入其它模块的语法有两种方式:

  1. 方式一:import…
  2. 方式二:from…import…
import…
1
2
3
4
import package_name.module_name as alias
# package_name: 包名
# module_name:模块名
# alias: 别名
from…import…
  1. 注意点:
1
2
3
【注意】
1> 当使用from...import导入一个模块中的多个成员,用逗号隔开: from utils.m1 import a, b;
2> 当使用from...import *时,可以在被导入的模块文件中用__all__=['elemnt1',...]列表来表示*要导出的元素名;
  1. 语法格式:
1
2
3
4
from [包名.]模块 import 函数/变量
from [包名] import 模块
# 不推荐用*
from [包名][模块] import *
模块导入示例
导入内建模块

以导入sys模块为例:

1
import sys
导入自定义模块

1> 创建utils包,在保重创建一个test模块:

1
desc = 'from test module'

2> 如果创建一个与test模块不同包的模块main,来使用test模块的功能:

1
2
import utils.test
print(utils.test.desc)

如果导入的模块包级别特别深时,可以使用别名:

1
2
import utils.test as test
print(test.desc)

3> 如果创建一个与test模块同包的模块main,来使用test模块的功能:

1
2
import test
print(test.desc)

作用域

作用域有公开的和私有的作用域,来控制模块内的属性是否能被外界访问;

公开作用域
  • 模块内正常的函数和变量名是公开的(public),可以被直接引用;
  • 模块内类似xxx这样的变量是特殊变量,可以被直接引用,但是有特殊用途,我们自己的变量一般不要用这种变量名;
1
2
【特殊变量】
模块文件可以定义,如__author__,__name__,__doc__等特殊变量,来表示模块的一些标识;
私有作用域

在Python中,是通过下滑线(“_”)前缀来实现私有作用域(private),被私有化的属性不能被外部访问;

1
private函数或变量不应该被别人引用,只需给名称加单下划线("_")前缀即可;

安装第三方模块

pip包管理工具

在Python中,安装第三方模块,是通过包管理工具pip完成的。

1
2
3
4
【pip的安装】
1> Mac或Linux,pip在安装python时默认安装了;
Mac或Linux上有可能并存Python 3.x和Python 2.x,python2只支持pip,python3既支持pip3也支持pip 。
2> windows安装python时,确保安装时勾选了pip和Add python.exe to Path。
第三方库的获取
  • 官方地址:
    一般第三方库都会在Python官方的pypi.python.org网站注册,可以直接在此网站搜索需要的库;
  • anaconda官网 :一个基于Python的数据处理和科学计算平台,它已经内置了许多非常有用的第三方库,我们装上Anaconda,就相当于把数十个第三方模块自动安装好了,Anaconda会把系统Path中的python指向自己自带的Python,可直接使用python命令进入。
安装第三方库
  • 安装Pillow库,参考官方文档,安装命令如下:
1
$ pip install Pillow
模块搜索路径Path

当使用import加载一个模块时,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错ImportError。

默认搜索路径

默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys模块的path变量中;

1
2
3
【查看搜索路径】
>>> import sys
>>> sys.path
自定义模块搜索路径

添加自己的搜索目录,有两种方法:

1> 方法一:是直接修改sys.path,添加要搜索的目录(这种方法是在运行时修改,运行结束后失效)。

1
2
>>> import sys
>>> sys.path.append('/Users/michael/my_py_scripts')

2> 方法二:是设置环境变量PYTHONPATH,该环境变量的内容会被自动添加到模块搜索路径中。设置方式与设置Path环境变量类似。注意只需要添加你自己的搜索路径,Python自己本身的搜索路径不受影响。

面向对象编程

  • 面向对象的设计思想是抽象出类(Class),根据Class创建实例(Instance)对象;
  • 面向对象的抽象程度又比函数要高,因为一个Class既包含数据,又包含操作数据的方法。

类和实例

类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;

创建类
语法格式

定义类的语法格式如下:

1
2
class ClassName(object):
pass

【注释】

  • ClassName:类名通常是大写开头的单词,不同于变量名、函数名,类名推荐驼峰命名;
  • (object):表示该类是从哪个类继承下来的,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
示例
1
2
3
# 创建类
class Student(object):
pass
类实例

定义好了类,就可以通过ClassName()创建类的实例对象,即创建实例是通过类名+()实现的;

  • 类的实例可以自由的访问类公开的成员变量和方法;
  • 可以自由地给一个类的实例变量绑定属性, 此时只作用于此实例,不会影响到其它实例;
示例
1
2
3
4
5
6
7
8
# 创建类
class Student(object):
pass

# 类的实例对象
student = Student()
# 可以自由地给一个实例变量绑定属性
student.name = 'ethan'
类的构造函数

类可以起到模板的作用,可以定义类的构造函数(比普通函数多了个默认不用传的self参数,代表类自身的实例),让类实例化时传入一些默认的属性值,作为类初始化属性的值。

构造函数的默认参数self可以直接给当前类添加新属性

构造函数语法
1
2
3
4
5
通过定义一个特殊的__init__方法,在创建实例的时候,就把设定的必须的属性绑上去;
构造函数格式:def __init__(self,*args)
【构造函数的参数】
- self:默认参数,不需要传;
- *args: 其他参数,必须传;

例如:在创建Student类实例的时候,传入name、score属性

1
2
3
4
5
6
7
8
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score

student = Student('ethan', 99)
print(student.name, student.score)
# ethan 99
类变量

概述:类变量必须通过“类名.变量名”来访问和赋值;

  • 获取类的所有变量组成的字典:通过类名.__dict__来访问,返回类所有成员组成的字典;
  • 类方法访问类变量:
1
2
1> 类名.变量名
2> self.__class__.变量名;
1
2
3
4
5
6
7
8
9
class Student(object):
name = '张三'

def __init__(self):
pass

Student.name = 'hello'
print(Student.name) # hello
print(Student.__dict__) # {}
实例变量

概述:实例变量必须通过“实例对象.变量名”来访问和赋值;

  • 实例变量的定义:必须在类中用self.变量名定义,才会被添加到实例变量字典中;
  • 实例方法中使用实例变量:最好加self.变量名;
  • 实例变量和类变量的访问:首先会在实例变量里找,没有用self.varname定义时,python会到类中再查找,返回同名的类变量,如果类中也没有,会到父类中继续查找
  • 实例变量字典:通过对象.__dict__来访问,返回字典;
实例方法

实例方法相对于函数,增加了一个默认参数self,可以直接操作self给当前类添加新属性;

定义类实例的方法

实例方法实际就是函数添加了self作为第一个参数,调用时除了self不用传递,其他参数正常传入;

1
2
3
4
5
6
7
8
9
10
11
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score

# 实例方法封装:打印分数score
def print_score(self):
print(self.score)

student = Student('ethan', 99)
student.print_score()
类名访问实例方法
1
2
3
4
5
6
7
8
9
10
11
12
class Student(object):
name = '张三'

def __init__(self):
self.__name = "ethan"

def print_hello(self):
print("hello," + self.__name)

s = Student()
s.print_hello()
Student.print_hello(s)
实例方法访问类变量
1
2
3
1. 方法内部直接使用:类名.变量名;
2. 方法内部使用__class__内置函数来访问:self.__class__.变量名;
3. 实例方法操作类变量:会直接改变类变量;
1
2
3
4
5
6
7
8
9
10
11
class Test(object):
name = 'ethan'

def set_name(self, name):
# Test.name = name
self.__class__.name = name

t = Test()
t.set_name("张三")
print(Test.name)
# 张三
实例方法访问实例变量
1
self.变量名
类方法

类方法定义时,也需要用一个标识符cls作为第一个参数(cls同self类似也可以是任意的字符),cls代表类本身,但是方法需要用装饰器@classmethod来声明;

1
2
3
4
5
6
7
8
9
10
11
12
class Test(object):
sum = 0

# 类方法
# @classmethod:装饰器,标识类方法
@classmethod
def do_sum(cls):
# 类方法操作类变量
cls.sum += 1
print(cls.sum)

Test.do_sum()
要点
  • 在python中类的对象访问类方法是可以的,不推荐使用;
  • 类方法可以访问静态方法;
  • 类方法不能直接访问实例方法;
类的静态方法

概述:静态方法可以同时被类和对象访问,不同于类方法和实例方法的是不需要默认有第一个参数代表静态方法,但是需要装饰器@staticmethod作为静态方法的标识;

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test(object):

# 静态方法可以同时被对象和类访问
@staticmethod
def print_info():
print('调用了静态方法')


# 对象访问静态方法
test = Test()
test.__class__.print_info()
# 类名访问静态方法
Test.print_info()
要点
  • 静态方法可以用类方法代替使用;
  • 静态方法不可以直接访问实例方法;
  • 静态方法可以访问类方法:类名.方法名();

类的访问限制

私有变量
私有变量

如果要让内部属性不被外部访问,可以把属性的名称前加上双下划线,在Python中,实例的变量名如果以开头,就变成了一个私有变量(private)。

要点
  • 私有变量只有类的内部可以访问,外部实例不能访问。
  • 可以通过对外暴露返回私有变量的方法,供外部调用来访问私有成员;
  • 注意:同时以双下划线做前缀和后缀的变量是特殊变量,类的外部实例可以直接访问。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Student(object):
# 私有变量
__age = 28

def __init__(self, name):
# 私有变量
self.__name = name
self.sex = 'man'

def print_info(self):
print(self.__name, self.__age)

# 返回私有变量name
def get_name(self):
return self.__name

# 修改私有变量name
def set_name(self, name):
self.__name = name

student = Student('ethan')
student.print_info() # ethan 28
student.set_name("令狐冲")
print(student.get_name()) # 令狐冲
单下划线前缀属性

通常模块的私有成员使用单下划线做前缀,类似的,在类中,以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。

私有方法

同私有变量

访问私有属性

强烈建议不要这么干

Python私有属性不是绝对的,可以通过如下格式访问:

1
实例名._类名__方法名()或属性名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 示例
class Animal(object):
__name = '二哈'
__name2 = '二哈'
_age = 10

def __init__(self, name, age):
self.__name = name
self._age = age

def __print_name(self):
print(self.__name)


dog = Animal("三哈", 15)
print(dog._Animal__name)
print(dog._Animal__print_name())

继承

  • Python的类也支持类似java中的继承,在定义类时,类名后面紧跟的圆括号里的类名就是当前定义的类需要继承的父类,通常无需继承时,默认要写object表示所有类的父类;
  • Python允许使用多重继承,继承多个类时,用逗号分隔即可;
单继承示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 示例1
class Animal(object):
def run(self):
print('Animal is running...')

class Dog(Animal):
pass

class Cat(Animal):
pass

dog = Dog()
dog.run()

cat = Cat()
cat.run()
多继承示例
1
2
3
4
5
6
7
8
9
10
11
12
13
# 示例2
class Animal(object):
pass

class Runnable(object):
def run(self):
print('it is running...')

class Dog(Animal, Runnable):
pass

dog = Dog()
dog.run()

多态

同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。简单的说,多态就是用基类的引用指向子类的对象。

例如:当子类和父类都存在相同方法时,子类的方法会覆盖了父类的同名方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Animal(object):
def run(self):
print('Animal is running...')

class Dog(Animal):
def run(self):
print('Dog is running...')

class Cat(Animal):
def run(self):
print('Cat is running...')

dog = Dog()
dog.run()

cat = Cat()
cat.run()
# Dog is running...
# Cat is running...

对象的类型判断

判断对象的类型,同判断变量类型类似,两种方法:

  1. type(x)
  2. isinstance(x,type)
type()
  • 基本的type()方法可以判断一些基本的数据类型,也可以判断对象的类型:
1
2
3
4
5
6
class Animal(object):
pass

dog = Animal()
# 注意这种方式无法用于判断继承类型
print(type(dog) == Animal)
  • 判断一个对象是否是函数,可以使用types模块:
1
2
3
4
5
6
7
8
9
10
11
12
13
import types

def fn():
pass

# 函数类型
print(type(fn) == types.FunctionType)
# 内建函数类型
print(type(abs) == types.BuiltinFunctionType)
# lambda表达式类型
print((type(lambda x: x) == types.LambdaType))
# 生成器类型
print(type((x for x in range(10))) == types.GeneratorType)
isinstance()

建议一直使用来判断对象或者变量的数据类型;

  • 对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数;
  • 能用type()判断的基本类型也可以用isinstance()判断;
1
2
3
4
5
6
7
8
9
class Animal(object):
pass

class Dog(Animal):
pass

dog = Dog()
print(isinstance(dog, Dog))
print(isinstance(dog, Animal))

获取对象的信息

获取所有属性和方法

Python提供内置函数dir()来获取对象的所有属性和方法,dir()返回一个包含字符串的list;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Animal(object):
__name = '二哈'
_age = 10

def __init__(self, name, age):
self.__name = name
self._age = age

class Dog(Animal):
def get_name(self):
return self.__name

dog = Dog("三哈", 15)
print(dir(dog))

# ['_Animal__name', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_age', 'get_name']
属性变量的设置和获取
  • 获取对象的属性:getattr
1
2
# 如果不存在,会抛异常,推荐设置默认值
getattr(obj,str,default)
  • 设置对象的属性:setattr
1
setattr(obj,str,default)

示例:

1
2
3
4
5
6
class Animal(object):
alias = ''

dog = Animal()
setattr(dog, 'alias', '小名')
print(getattr(dog, 'alias', '默认'))
判断是否包含某属性

hasattr()内置函数用来判断某对象是否包含某个属性;

1
2
3
4
5
6
class Animal(object):
alias = ''

dog = Animal()
print(hasattr(dog, 'alias')) # True
print(hasattr(dog, 'name')) # False

实例绑定属性和类绑定属性

  1. 实例属性属于各个实例所有,实例之间互不干扰;
  2. 类属性属于类所有,所有实例共享一个属性;
  3. 不要对实例属性和类属性使用相同的名字,否则将产生难以发现的错误。
实例绑定属性

由于Python是动态语言,根据类创建的实例可以任意绑定属性。

绑定方式
  • 给实例绑定属性的方法是通过实例变量,或者通过self变量;
1
2
3
4
5
6
7
8
class Student(object):
def __init__(self, name):
# 绑定name属性
self.name = name

s = Student('Bob')
# 绑定score属性
s.score = 90
  • 通过setattr内置函数设置实例的属性;
限制允许绑定的属性
1
2
3
1> Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性;
2> __slots__赋值一个元组,元组中是存放的是允许添加的属性名称;
3> __slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的,除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__。
1
2
3
4
5
6
7
8
9
10
# 示例
class Student(object):
# 限制实例的属性
# 用tuple定义允许绑定的属性名称
__slots__ = ('name', 'age')
pass

student = Student()
student.age = 18
print(student.age)
类绑定属性
  • 直接在class中定义属性变量,这种属性变量是类属性,可以直接通过类名.属性变量名访问;
  • 当我们定义了一个类属性后,这个属性虽然归类所有,类的所有实例都可以访问到。
1
2
3
4
5
6
7
8
class Test(object):
name = 'ethan'

print(Test.name) # ethan
Test.age = 11
print(Test.age) # 11
t = Test()
print(t.__class__.age) # 11

实例绑定方法和类绑定方法

实例绑定方法

类的实例的操作,不影响类的其它实例;

直接给一个初始化的实例绑定的方法,对另一个实例是不起作用,可以使用types模块的MethodType函数给实例对象绑定方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from types import MethodType

class Student(object):
name = 'miao'

# 给实例绑定一个方法
# 定义一个函数作为实例方法
def set_age(self, age):
self.age = age

s = Student()
s.set_age = MethodType(set_age, s)
s.set_age(25)
print(s.age)
类绑定方法

可以定义以个函数,然后通过类名.方法名直接给类绑定一个方法;

1
2
3
4
5
6
7
8
9
10
class Student(object):
name = 'miao'

# 以给class绑定方法
def set_score(self, score):
self.score = score

Student.set_score = set_score
s.set_score(100)
print(s.score)

类方法的装饰器

装饰器(decorator)可以给函数动态加上功能,对于类的方法,装饰器一样起作用。

@property属性读写限制和检查

Python内置的@property装饰器就是负责把一个方法变成属性调用的, 并且可以限制属性的读写;

  1. 在方法上添加@property,负责把此属性的getter方法变成属性的读取,此时只读。
  2. @属性名.setter,负责把一个setter方法变成属性赋值;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Student(object):

def __init__(self, birth):
self._birth = birth

@property
def birth(self):
return self._birth

@birth.setter
def birth(self, value):
self._birth = value

@property
def age(self):
return 2015 - self._birth

# birth是可读写属性,而age就是一个只读属性
s = Student(2008)
print(s.age) # 7
s.birth = 2014
print(s.age) # 1
s.age = 12 # error

类可定制

Python为类class内部提供了一些特殊方法用来定制类,允许重写这些特殊方法,为类的实例添加一些特殊的功能。

类的可定制方法官方文档

__str__

字符串输出实例

1
2
1> __str__用来输出类实例字符串信息,通常我们可以重新定义它来返回一个规则的字符串;
2> __repr__用来输出类实例变量的字符串表示信息,通常__str__()和__repr__()代码都是一样的;

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Student(object):
def __init__(self, name):
self.name = name

def __str__(self):
return 'Student object (name: %s)' % self.name

__repr__ = __str__

print(Student('Michael'))
s = Student('Lily')
print(s)
# Student object (name: Michael)
# Student object (name: Lily)
__iter__

类添支持迭代

1
2
3
如果一个类想被用于for ... in循环,类似list或tuple那样,就必须实现一个__iter__()方法,
该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法
拿到循环的下一个值,直到遇到StopIteration错误时退出循环。

示例:以斐波那契数列为例,写一个Fib类,可以作用于for循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1 # 初始化两个计数器a,b

def __iter__(self):
return self # 实例本身就是迭代对象,故返回自己

def __next__(self):
self.a, self.b = self.b, self.a + self.b # 计算下一个值
if self.a > 100000: # 退出循环的条件
raise StopIteration()
return self.a # 返回下一个值

for n in Fib():
print(n)
__getitem__

类实例支持索引访问

__getitem__()传入的参数可能是一个int,也可能是一个切片对象slice

  1. 当接收的是int参数时,仅支持下标访问:
1
2
3
4
5
6
7
8
9
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a

f = Fib()
print(f[10])
  1. 支持切片访问:接收的参数也可以是切片对象,需要判断做处理;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Fib(object):
def __getitem__(self, n):
if isinstance(n, int): # n是索引
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
if isinstance(n, slice): # n是切片
start = n.start
stop = n.stop
if start is None:
start = 0
a, b = 1, 1
result = []
for x in range(stop):
if x >= start:
result.append(a)
a, b = b, a + b
return result

f = Fib()
print(f[0:5])
拓展
1
2
1> 与__getitem__对应的是__setitem__()方法,把对象视作list或dict来对集合赋值。
2> __delitem__()方法,用于删除某个元素。

通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。

__getattr__

类实例调用未定义的属性和方法处理

正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。

要点
1
2
3
4
1> 重写__getattr__()方法,可以对未知属性和方法进行处理;
2> 如果属性未定义,也没有在__getattr__()中处理,也会返回None;
3> 如果方法未定义,也没有在__getattr__()中处理,则会抛出错误;
4> 注意:只有在没有找到属性的情况下,才调用__getattr__,已有的属性;
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Student(object):
def __init__(self):
self.name = 'Michael'

def __getattr__(self, attr):
if attr == 'score':
return 99
elif attr == 'age': # 返回函数
return lambda: 25

s = Student()
print(s.money) # None
print(s.score) # 99
print(s.age()) # 25
print(s.agex()) # error
__call__

可调用对象

要点
1
2
3
4
1> 任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用;
2> __call__()还可以定义参数。
3> 对实例进行直接调用就好比对一个函数进行调用一样;
4> 能被调用的对象就是一个Callable对象;

示例

1
2
3
4
5
6
7
8
9
10
class Student(object):
def __init__(self, name):
self.name = name

def __call__(self):
print('My name is %s.' % self.name)

s = Student('Michael')
s()
# My name is Michael.
可调用对象

通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。

1
2
3
4
>>> callable(Student())
True
>>> callable(None)
False

枚举类

Python提供了Enum枚举类来实现每个类中的常量都是class的一个唯一实例;

概念要点
概念
  • 枚举类本质还是一个类,需要继承enum模块的Enum类或IntEnum类,枚举成员名称大写;
1
from enum import Enum,IntEnum,unique(标识枚举标签的值不能相等)
枚举值类型
  • Enum类型的枚举成员的值可以是字符串、数字类型;
  • IntEnum类型的枚举成员的值只能是int类型;
使用注意
  • Enum可以把一组相关常量定义在一个class中,且class不可变,而且成员可以直接比较。
  • 枚举成员是不可更改的常量;
  • 既可以用成员名称引用枚举常量,又可以直接根据value的值获得枚举常量;
  • 枚举不能实例化,它通过单例模式设计的;
  • 枚举有两个成员值相等时,第二个重复的是第一个的别名,一般别名枚举标签不会被打印,获取到的都是第一次出现的标签名;
  • 如果需要打印别名可以遍历:枚举类.__members__(只打印枚举标签的名称),枚举类.__member__.items()(打印的是各个枚举成员的所有内容元组);
简单的枚举类
1
2
3
4
5
6
7
8
9
10
from enum import Enum

# Enum(cls,value)
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

# 遍历枚举类的所有成员
# name: cls类名,member 枚举类中的常量值
for name, member in Month.__members__.items():
# value属性则是自动赋给成员的int常量,默认从1开始计数。
print(name, '=>', member, ',', member.value)
自定义枚举类

如果需要更精确地控制枚举类型,可以从Enum派生出自定义类;

自定义枚举类,需要使用@unique装饰器帮助我们检查保证没有重复值。

1
2
3
4
5
6
7
8
9
10
11
from enum import Enum, unique

@unique
class Weekday(Enum):
Sun = 0 # Sun的value被设定为0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6
访问枚举类
1
2
3
4
5
6
7
8
9
10
1. 获取枚举的标签名称:枚举类名.标签名.name
2. 获取枚举标签对应的值:枚举类名.标签.value
3. 获取枚举的标签:枚举类名['标签名']、枚举类名(标签值)、枚举类.标签
4. 遍历枚举的所有标签:for e in 枚举类;
5. 遍历枚举的所有成员包括别名标签名:枚举类.__members__;
6. 遍历枚举的所有成员包括别名标签详细信息:枚举类.__members__.items();
7. 枚举不支持大小和值的比较,可以做等值比较 :枚举类.标签==枚举类.标签;
8. 枚举可做身份比较:枚举类.标签 is 枚举类.标签;
9. 枚举中通过标签值获取枚举标签:枚举类(value);
10. 约束枚举标签的值不能重复:使用装饰器 @unique;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from enum import Enum, unique

# @unique装饰器可以帮助我们检查保证没有重复值。
@unique
class Weekday(Enum):
Sun = 0 # Sun的value被设定为0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6

print(Weekday.Mon)
print(Weekday(1))
print(Weekday['Mon'])
# 获取枚举对应int值
print(Weekday.Mon.value)
# 枚举比较
day1 = Weekday.Mon
print(day1 == Weekday.Mon)
print(Weekday(1) == Weekday.Mon)
print(Weekday(1) == day1)

'''
结果:
Weekday.Mon
Weekday.Mon
Weekday.Mon
1
True
True
True
'''

动态创建类

使用type函数

通过type()函数创建的类和直接写class是完全一样的;

type()函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()函数创建出Hello类,而无需通过class Hello(object)…的定义;

语法
1
type('类名',(object,父类元组...),属性字典)

要创建一个class对象,type()函数依次传入以下3个参数:

1
2
3
1. class的名称;
2. 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
3. class的方法名称与函数绑定的dict。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 先定义函数
def fn(self, name='world'):
print('Hello, %s.' % name)

d = {
'hello': fn,
'name': 'ethan'
}

# 动态代码创建Hello class
Hello = type('Hello', (object,), d)

h = Hello()
h.hello()
print(h.name)
使用metaclass

除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass(元类)。

定义和使用元类的步骤:

  • 元类需要继承type类,并重写__new__()方法用于创建类实例;
  • 元类需要重写的__new__()方法实际是增加对attrs逻辑处理,最终回调type.__new__(mcs, name, bases, attrs),来应用修改后的实例化逻辑;
  • 自定义一个类来使用元类时,只需要给自定类的父类元组中添加metaclass属性来指向已定义的元类,那么自定义类实例化时,就会调用元类定义的__new__()方法来进行实例化;

应用metaclass的类的mataclass特性会继续应用到子类;

__new__()

__new__(mcs, name, bases, attrs)方法接收到的参数依次是:

  1. mcs: 当前准备创建的元类模板自己的实例对象;
  2. name: 类的名字;
  3. bases: 类继承的父类集合;
  4. attrs: 类的属性和方法集合。
示例
  • metaclass给自定义的MyList增加一个add方法来拓展list:

    不要使用元类来添加方法,此处只做演示;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# metaclass是类的模板,所以必须从`type`类型派生:
class ListMetaclass(type):
# 指定在创建MyList时,要通过ListMetaclass.__new__()来创建
def __new__(mcs, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(mcs, name, bases, attrs)

# 指示使用ListMetaclass来定制类,传入关键字参数metaclass
class MyList(list, metaclass=ListMetaclass):
pass

L = MyList()
L.add('hello')
L.add('world')
print(L)
  • metaclass可以隐式地继承到子类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# metaclass是类的模板,所以必须从`type`类型派生:
class ListMetaclass(type):
# 指定在创建MyList时,要通过ListMetaclass.__new__()来创建
def __new__(mcs, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(mcs, name, bases, attrs)

# 指示使用ListMetaclass来定制类,传入关键字参数metaclass
class MyList(list, metaclass=ListMetaclass):
pass

# metaclass可以隐式地继承到子类,但子类自己却感觉不到
class XList(MyList):
pass

L = XList()
L.add('hello')
L.add('world')
print(L)
元类实现ORM

ORM全称“Object Relational Mapping”,即对象-关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样,写代码更简单,不用直接操作SQL语句。

要编写一个ORM框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。

实现ORM简单框架的代码加注释,详细如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# Field: 存储数据表的映射字段名和类型
class Field(object):

# name 字段名
# column_type 对应数据库的数据类型
def __init__(self, name, column_type):
self.name = name
self.column_type = column_type

def __str__(self):
return '<%s:%s>' % (self.__class__.__name__, self.name)

# 字符串类型(varchar(100))
class StringField(Field):

def __init__(self, name):
super(StringField, self).__init__(name, 'varchar(100)')

# 整数类型(bigint)
class IntegerField(Field):

def __init__(self, name):
super(IntegerField, self).__init__(name, 'bigint')


# 元类模板:
class ModelMetaclass(type):

# 重写使用此元类模板的子类创建实例对象的方法
def __new__(mcs, name, bases, attrs):
# 尚未实例化时的处理
if name == 'Model':
return type.__new__(mcs, name, bases, attrs)

# 实例化后的处理
# 打印实例化的类名
print('Found model: %s' % name)
# 将attrs转存到mappings实例属性对的映射字典:存储数据字段和值
mappings = dict()
for k, v in attrs.items():
if isinstance(v, Field):
print('Found mapping: %s ==> %s' % (k, v))
mappings[k] = v
# 清空attrs
for k in mappings.keys():
attrs.pop(k)
# 给attrs重设属性
attrs['__mappings__'] = mappings # 保存属性和列的映射关系
attrs['__table__'] = name # 假设表名和类名一致

# 重新利用type创建实例
return type.__new__(mcs, name, bases, attrs)


# 使用元类作为模板,统一对外暴露对数据处理的方法和属性
class Model(dict, metaclass=ModelMetaclass):

def __init__(self, **kw):
super(Model, self).__init__(**kw)

def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Model' object has no attribute '%s'" % key)

def __setattr__(self, key, value):
self[key] = value

# 插入数据到数据库的sql
def save(self):
fields = []
params = []
args = []
for k, v in self.__mappings__.items():
fields.append(v.name)
params.append('?')
args.append(getattr(self, k, None))
sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
# 打印最终的插入sql
print('SQL: %s' % sql)
print('ARGS: %s' % str(args))


# testing code:
class User(Model):
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')


u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
u.save()

抽象类

定义抽象类

定义一个类,并指定抽象类的metaclass=ABCMeta(Abstract Base Classes)即表示抽象类;

  • 抽象类不能直接被实例化;
  • 抽象类中使用@abstractmethod装饰器标识的方法时抽象方法,方法体使用pass无需实现,但是子类必须实现抽象方法;
  • 可以直接使用继承抽象类的方式来直接使用抽象类;
  • 抽象类中使用@property+@abstractmethod装饰的方法称为抽象属性;
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from abc import ABCMeta, abstractmethod

# 定义一个抽象类(不能直接被实例化)
# ABCMeta(Abstract Base Classes)
class People(metaclass=ABCMeta):
# 抽象属性
@property
@abstractmethod
def name(self):
pass

# 定义抽象方法,无需实现功能,但是子类必须实现该方法
@abstractmethod
def get_sex(self):
pass

# 继承的方式让类实现实现抽象基类
class Student(People):
def __init__(self, name):
self._name = name

@property
def name(self):
return self._name

def get_sex(self):
return '男'

s = Student('ethan')
print(s.name) # ethan
print(s.get_sex()) # 男

错误处理

  • Python所有的错误都是从BaseException类派生的;
  • 只有在必要的时候再使用错误类型,尽量使用Python内置的错误类型。

捕获和处理错误

Python异常处理使用try…except…else…finally,try语句块内是需要被检查的代码, except捕获的异常类型可以有多个,else是没有异常时的处理,finally每次都会执行;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import logging

class MyError(BaseException):
pass

try: # try区域是需要被检查的代码
print('异常检测区...')
r = 10 / 0
print('result:', r)
except ZeroDivisionError as e: # 具体错误
raise MyError('抛出一个自定义错误')
except TypeError as e:
logging.exception(e)
except Exception as e:
# 抛出异常,将终端程序
raise Exception('抛出异常,将终端程序')
except BaseException as e:
# logging模块打印异常信息
logging.exception(e)
else:
print('没有错误代码块')
finally:
print('finally...')
print('END')

错误调试

断言错误

断言使用assert标识的语句,assert后跟表达式和错误信息,如果表达式返回True说明断言成功,否则就是断言失败,断言失败标识后面的语句就会报错;

注意:断言失败后会抛出异常,中断程序,不推荐

1
2
3
4
5
6
def foo(s):
n = int(s)
assert n != 0, 'n is zero!'
return 10 / n

foo(2)
关闭断言

启动Python解释器时可以用-O参数来关闭assert,关闭后,你可以把所有的assert语句当成pass来看。

1
$ python -O err.py
打印错误日志
  • 使用logging模块,来打印错误日志,好处是不会抛出错误中断程序;
  • logging模块有debug,info,warning,error等几个级别的信息;
1
2
3
4
5
# 导入logging模块
import logging
# 配置日志显示信息的级别
logging.basicConfig(level=logging.INFO)
logging.info('输出一段错误信息')
pdb命令调试

IDE调试

PyCharm和VScode都支持很智能的调试功能;

参考

常见的错误类型和继承关系官方文档

正则表达式

概述

正则表达式由普通字符和元字符组成,普通字符就是普通的字符,元字符是正则规定的特殊字符,Python提供re模块来操作正则表达式的模块;

正则元字符

转义字符

正则表达式使用斜杠(\)来表示转义字符;

边界符
元字符 描述
^ 匹配正则表达式的开始
$ 匹配正则表达式的结束
数量限制符
元字符 描述
* 匹配此元字符前面的子表达式任意次
+ 匹配此元字符前面的子表达式一次或多次
? 匹配此元字符前面的子表达式零次或一次,也可做非贪婪匹配限制符
{n} 匹配n(n>=0)次
{n,} 至少匹配n(n>=0)次
{n,m} 最少匹配n次,最多匹配m次(n<=m,n>=0,m>=0)
匹配表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
. 匹配除“\r\n”之外的任何单个字符,要匹配包括“\r\n”在内的任何字符,请使用像“[\s\S]” 
[xyz] 匹配[]内包含的任意一个字符
[^xyz] 匹配未包含在[]内的任意字符
[a-z] 匹配指定范围内的任意字符(例如:[a-zA-Z0-9])
[^a-z] 匹配任何不在指定范围内的任意字符
\d 匹配一个数字字符,等价于[0-9]
\D 匹配一个非数字字符
\w 匹配包括下划线的任何单词字符,类似但不等价于“[A-Za-z0-9_]”,这里的"单词"字符使用Unicode字符集
\W 匹配任何非单词字符,等价于“[^A-Za-z0-9_]”
\s 匹配任何不可见字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]
\S 匹配任何可见字符。等价于[^ \f\n\r\t\v]
() 将括号中的表达式定义为一个组,一个表达式最多9个组
| 将两个匹配条件进行逻辑“或”运算(例如:(him丨her)匹配him或her)
获取和非获取匹配
1
2
3
4
5
6
7
8
| 元字符 | 描述 |
| :--- | :--- |
|(pattern) | 匹配pattern并获取这一匹配;<br />如果要匹配的正则有多个(pattern)则每个pattern单独获得一个结果,<br />组成元组作为符合条件的一组结果) |
| (?:pattern) | 非获取匹配,匹配pattern但不获取匹配结果,不进行存储供以后使用;<br /> 例如“industr(?:y\|ies)”就是一个比“industry\|industries”更简略的表达式。 |
| (?=pattern) | 非获取匹配,正向肯定预查;<br />在任何匹配pattern的字符串开始处匹配查找字符串,该匹配不需要获取供以后使用。<br />例如:“Windows(?=95\|98\|NT\|2000)”<br /> 能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”。<br />预查不消耗 字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次,<br />匹配的搜索而不 是从包含预查的字符之后开始。 |
| (?!pattern) | 非获取匹配,正向否定预查;<br />在任何不匹配pattern的字符串开始处匹配查找字符串,<br />该匹配不需要获取供以后使用;<br />例如:“Windows(?!95\|98\|NT\|2000)”能匹配“Windows3.1”中的<br />“Windows”,但不能匹配“Windows2000”中的“Windows”。 |
| (?<=pattern) | 非获取匹配,反向肯定预查,与正向肯定预查类似,只是方向相反;<br />例如:“(?<=95\|98\|NT\|2000)Windows”能匹配“2000Windows”中<br />的“Windows”,但不能匹配“3.1Windows”中的“Windows”) |
| (?<!pattern) | 非获取匹配,反向否定预查,与正向否定预查类似,只是方向相反;<br />(例如“(?<!95\|98\|NT\|2000)Windows”能匹配“3.1Windows”中的“Windows”,<br />但不能匹配“2000Windows”中的“Windows” |
其它匹配字符
元字符 描述
\f 匹配一个换页符。等价于\x0c和\cL
\n 匹配一个换行符。等价于\x0a和\cJ
\r 匹配一个回车符。等价于\x0d和\cM
\t 匹配一个制表符。等价于\x09和\cI
\v 匹配一个垂直制表符。等价于\x0b和\cK
\xn 匹配十六进制数n的转义值(例如:“\x41”匹配“A”)

转义字符问题

由于Python的字符串本身也用\转义,因此强烈建议使用Python的r前缀来写正则,就不用考虑转义的问题了:

1
2
s = 'ABC\\-001' # 'ABC\-001'
s = r'ABC\-001' # 'ABC\-001'

re模块

Python提供re模块,提供所有与正则表达式相关的功能;

正则匹配方法
查找和匹配

match、search、findall

1.语法:

1
2
3
4
5
6
7
8
9
re.match(pattern,string,flag):判断字符串的首个元素是否匹配pattern,不匹配直接返回None,匹配时返回一个结果对象;
re.search(pattern,string,flag):搜索整个字符串查找第一个匹配的字符串并返回它的结果对象,没有匹配的则返回None;
re.findall(pattern,string):找出string中所有匹配的元素,返回结果[]列表;
re.findall(pattern,string,flag):找出string中所有匹配的元素,返回结果[]列表;
【参数解析】
flag:代表匹配模式,可以用|(或)符号一次使用多个flag.
常见flag匹配模式有:
1> re.I:不区分大小写;
2> re.S:让.(点)可以匹配\n;

2.示例:

1> match

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import re

regex = r'^\d{3}\-\d{3,8}$'
test = '010-12345'

# 打印匹配结果
r = re.match(regex, '010-12345')
print(r)

# 逻辑判断是否匹配
if re.match(regex, test):
print('ok')
else:
print('failed')

2> findall

1
2
3
4
5
import re

a = 'hello ethan, hello jason'
b = re.findall('hello', a)
print(b)
分组匹配

除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。正则表达式中用()包裹的部分表示的就是要提取的分组(Group)。

要点

  • 如果正则表达式中定义了组,就可以在Match对象上用group()方法提取出子串来。
  • group(0)永远是原始字符串,group(1)、group(2)……表示第1、2、……个子串。

示例

  1. 普通分组和子串的提取:
1
2
3
4
5
6
7
8
9
>>> m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345')
>>> m
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
>>> m.group(0)
'010-12345'
>>> m.group(1)
'010'
>>> m.group(2)
'12345'
  1. 正则来识别合法的时间:
1
2
3
4
>>> t = '19:05:30'
>>> m = re.match(r'^(0[0-9]|1[0-9]|2[0-3]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])$', t)
>>> m.groups()
('19', '05', '30')
贪婪匹配

贪婪匹配模式:正则表达式默认匹配模式,尽可能多的匹配所搜索的字符串;

1
2
3
>>> re.match(r'^(\d+)(0*)$', '102300').groups()
('102300', '')
# 由于\d+采用贪婪匹配,直接把后面的0全部匹配了,结果0*只能匹配空字符串了。
非贪婪匹配

非贪婪匹配模式:尽可能少的匹配所搜索的字符串;

非贪婪模式限制符为问号?,即当?字符紧跟在任何一个其他限制符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。

1
2
3
例如:对于字符串“oooo”的贪婪和非贪婪匹配处理?
1> “o+”将尽可能多的匹配“o”,得到结果[“oooo”],
2> “o+?”将尽可能少的匹配“o”,得到结果 ['o', 'o', 'o', 'o']
1
2
3
# 示例
>>> re.match(r'^(\d+?)(0*)$', '102300').groups()
('1023', '00')
re模块其他操作函数
查找替换
  1. 语法:
1
2
3
4
5
6
7
8
9
10
11
re.sub(pattern,replace,string,count)
【参数】
> replace是字符串时:将string中符合pattern表达式的字符替换成replace;
> replace是函数时:接收string中满足替换条件pattern的所有结果所组成的value对象作为参数,通过value.group()方法拿到pattern符合表达式的结果字符;
【value.group解析】
value.group(index):index不传时默认0,获取匹配的结果字符;
如果pattern使用()分组,则返回结果同分组类似:
1> index=0时,返回的是完整匹配;
2> index>0时,返回对应第index组匹配结果;
value.groups():返回value对应的所有结果组成的元组;
> count:表示替换几个(默认0表示替换所有);
  1. 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import re

language = 'PythonC#12JavaC#3PHPC#21'

def convert(value):
print(value.groups())
print(value.group())
return '*'

# 普通匹配
r = re.sub(r'C#', convert, language)
# 分组匹配
r = re.sub(r'(C#)(\d)', convert, language)

print(r)
预编译该正则表达式

如果一个正则表达式要重复使用几千次,出于效率的考虑,我们可以预编译该正则表达式,接下来重复使用时就不需要编译这个步骤了,直接匹配:

编译后生成Regular Expression对象,由于该对象自己包含了正则表达式,所以调用对应的方法时不用给出正则字符串。

1
2
3
4
5
6
7
8
>>> import re
# 编译:
>>> re_telephone = re.compile(r'^(\d{3})-(\d{3,8})$')
# 使用:
>>> re_telephone.match('010-12345').groups()
('010', '12345')
>>> re_telephone.match('010-8086').groups()
('010', '8086')

正则切分字符串

用正则表达式切分字符串比用固定的字符更灵活;

1
2
3
4
5
6
7
# 普通分隔符切分,无法识别连续空格
>>> 'a b c'.split(' ')
['a', 'b', '', '', 'c']

# 使用正则切分字符串,可以识别连续空格
>>> re.split(r'\s+', 'a b c')
['a', 'b', 'c']

正则表达式收集

校验数字的表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. 数字:^[0-9]*$
2. n位的数字:^\d{n}$
3. 至少n位的数字:^\d{n,}$
4. m-n位的数字:^\d{m,n}$
5. 零和非零开头的数字:^(0|[1-9][0-9]*)$
6. 非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$
7. 带1-2位小数的正数或负数:^(\-)?\d+(\.\d{1,2})?$
8. 正数、负数、和小数:^(\-|\+)?\d+(\.\d+)?$
9. 有两位小数的正实数:^[0-9]+(.[0-9]{2})?$
10. 有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$
11. 非零的正整数:^[1-9]\d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^\+?[1-9][0-9]*$
12. 非零的负整数:^\-[1-9][]0-9"*$ 或 ^-[1-9]\d*$
13. 非负整数:^\d+$ 或 ^[1-9]\d*|0$
14. 非正整数:^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$
15. 非负浮点数:^\d+(\.\d+)?$ 或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$
16. 非正浮点数:^((-\d+(\.\d+)?)|(0+(\.0+)?))$ 或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$
17. 正浮点数:^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ 或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
18. 负浮点数:^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ 或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$
19. 浮点数:^(-?\d+)(\.\d+)?$ 或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$
校验字符的表达式
1
2
3
4
5
6
7
8
9
10
11
12
1. 汉字:^[\u4e00-\u9fa5]{0,}$
2. 英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
3. 长度为3-20的所有字符:^.{3,20}$
4. 由26个英文字母组成的字符串:^[A-Za-z]+$
5. 由26个大写英文字母组成的字符串:^[A-Z]+$
6. 由26个小写英文字母组成的字符串:^[a-z]+$
7. 由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$
8. 由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$
9. 中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
10. 中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$
11. 可以输入含有^%&',;=?$\"等字符:[^%&',;=?$\x22]+ 12 禁止输入含有~的字符:[^~\x22]+
12. 空白行的正则表达式:\n\s*\r (可以用来删除空白行)
Email地址
1
^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
域名
1
[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?
手机号码
1
^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$
IP地址
1
2
3
4
# 提取IP地址
\d+\.\d+\.\d+\.\d+
# 匹配IP地址
((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))

IO编程

文件读写

Python中文件读写的IO操作主要有以下步骤内容:

  1. 通过with open()函数打开文件,with的作用是自动关闭文件(等价于调用close函数关闭文件);
  2. 使用read或write进行读写操作;
  3. 读写文件的过程,最好捕获错误;
读文件示例

读一个文本文件,输出所有内容:

1
2
3
4
5
try:
with open('./test.txt', 'rt', encoding='utf-8', errors='ignore') as f:
print(f.read())
except IOError as e:
print(e)

等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
f = None
try:
# 内置函数open()函数用来打开文件对象
# 标示符'r'表示读
f = open('./test.txt', 'rt', encoding='utf-8', errors='ignore')
# read()一次读取文件的全部内容
print(f.read())
# close()关闭文件
f.close()
except IOError as e:
raise IOError('IO异常')
finally:
if f:
f.close()
读写文件的内置函数
open()

open函数用来打开文件准备进行操作,语法格式如下:

1
2
3
4
5
6
7
open(file, mode='r', buffering=None, encoding=None, errors=None, newline=None, closefd=True)
【参数】
> file: 将要打开的文件,可以是文件地址;
> mode: 操作文件的模式(r(读)、w(覆盖写)、x(创建文件并打开写)、a(追加写)、b(二进制)、t(默认文本)、+(更新模式读写))
注意:r/w/x可以与b/t拼接使用;
> encoding: 编码格式;
> error: 错误处理,可以设置为'ignore',忽略错误;

示例

1
2
# 打开读一个二进制文件
f = open('./test.jpg', 'rb')
read()

读文件,返回字符串,语法解释如下:

1
read(size): size不传时默认读取全部,否则按size大小读;
readlines()

调用readlines()一次读取所有内容并按行返回list。

1
2
for line in f.readlines():
print(line.strip()) # 把末尾的'\n'删掉

类似方法:readline()可以每次读取一行内容,

write()

写文件

1
write(str): str为写入的内容;

内存读写

StringIO

StringIO作用是在内存中读写str。

内存读写操作
  • write(str)方法将数据写入内存,调用此方法后读的位置会变成当前字符末尾,如果调用read()读的话,需要先调用seek(0)将读的位置重置才行;
  • getvalue()方法用于获得写入后的str;
1
2
3
4
5
from io import StringIO

f = StringIO()
f.write('hello world')
print(f.getvalue())
  • StringIO实例对象可以像读文件一调用read方法读,但是如果实例化后再调用写入方法则需要调用seek(0)重置读的位置;
1
2
3
4
5
6
7
8
9
10
11
12
# 实例化后读
from io import StringIO

f = StringIO("hello\nworld\n")

while True:
s = f.readline()
if s == '':
break
print(s.strip())

print('end')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 写入后再读,需要seek(0)
from io import StringIO

f = StringIO("hello\nworld\n")
print(f.read())

f.write("welcome read")
f.seek(0)
while True:
s = f.read()
if s == '':
break
print(s.strip())

print('end')
ByteIO

StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO。

读写操作
1
2
3
4
5
6
7
8
9
10
from io import BytesIO

f = BytesIO()
# 写入二进制
f.write('中文'.encode('utf-8'))
print(f.getvalue())

# 像读文件一样读
f_read = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
print(f.read())

操作系统和文件目录

Python的os模块封装了操作系统的目录和文件操作,要注意这些函数有的在os模块中,有的在os.path模块中。

获取操作系统信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os
# os模块的某些函数是跟操作系统相关

# 获取操作系统类型(nt:windows,posix:linux/macOs/unix)
print(os.name)

# linux获取详细的系统信息
# print(os.uname())

# 获取操作系统中定义的环境变量
print(os.environ)
# 获取某个环境变量的值
print(os.environ.get('PATH'))
print(os.environ.get('x', 'default'))
操作文件和目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import os

# 当前路径可以用'.'表示
# 获取当前模块的文件名
print(__file__)
# 当前系统的路径分隔符
print(os.sep)
# 操作文件和目录
# 查看当前目录的绝对路径
print(os.path.abspath('.'))
# 获取当前目录下所有文件和目录
print(os.listdir('.'))
# 判断是否是目录
print(os.path.isdir('./utils'))
# 判断是否是文件
print(os.path.isfile('./test.md'))

# 路径拼接:不要直接拼字符串,而要通过os.path.join()函数进行拼接
# 创建一个目录:
new_dir = os.path.join(os.path.abspath('.'), 'testdir')
os.mkdir(new_dir)
# 删掉一个目录:
os.rmdir(new_dir)
# split将最后面级别的目录或文件与前缀的路径分割
print(os.path.split('/Users/michael/testdir/file.txt'))
# 将文件路径加文件名作为整体和文件扩展名分割
print(os.path.splitext('/path/to/file.txt'))
# 文件重命名
os.rename('./test.txt', 'test.md')
# 删除一个文件
os.remove('test.py')
复制文件
  • os模块中没有复制文件的函数,但是读写文件可以完成文件复制,也可以使用shutil模块提供的copyfile()
  • shutil模块中找到很多实用函数,它们可以看做是os模块的补充。

序列化

pickling序列化
概念

Python提供了pickle模块来实现序列化。

我们把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flattening等等,都是一个意思。

  • 序列化:序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。
  • 反序列化:反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling。
序列化操作
  • pickle.dumps()方法把任意对象序列化成一个bytes;
  • pickle.loads()方法把序列化的bytes反序列化为Python对象;
  • pickle.dump()方法将一个对象序列化,并写入文件;
  • pickle.loads()方法从文件中读取byte并反序列化出对象;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pickle

d = dict(name='Bob', age=20, score=88)

# 对象的序列化和反序列化
b = pickle.dumps(d)
print(b)
print(pickle.loads(b))

# 序列化到文件,从文件中反序列化
with open('dump.txt', 'wb') as fw:
pickle.dump(d, fw)

with open('dump.txt', 'rb') as fr:
print(pickle.load(fr))

JSON序列化

json写法

注意:json对象的key和字符串value必须用双引号包裹;

1
2
3
json对象写法:{“key”:"value",....} 
json数组写法:[{“key”:"value",....} ,{}]
json字符串写法:json_str='{“key”:"value",....}'

json和python的数据类型

json和python的数据类型对应关系如下:

json python
object dict
array list
string str
number int或float
true True
false False
null None

处理json

json字符串转python对象

json.loads()函数是将json格式数据转换为字典

1
2
3
4
5
6
import json

json_str = '[{"key": "value"}]'
d = json.loads(json_str)
print(d)
# [{'key': 'value'}]
python对象转json字符串

json.dumps()函数是将一个Python数据类型列表进行json格式的编码;

1
2
3
4
5
import json

obj_py = [{'key': 'value'}]
print(json.dumps(obj_py))
# [{"key": "value"}]
处理中文

json.dumps接收ensure_ascii参数表示是否使用ascii编码,默认为True时不能正确显示中文,如果显示中文请设置为False;

1
2
3
obj = dict(name='小明', age=20)
s = json.dumps(obj, ensure_ascii=False)
print(s)
读写json文件
  • json.dump()函数将json信息写入.json文件;
  • json.load()函数从.json文件中读取json信息;
1
2
3
4
5
6
7
8
9
10
11
import json

# json.dump()函数的使用,将json信息写进文件
json_info = "{'age': '12'}"
file = open('1.json', 'w', encoding='utf-8')
json.dump(json_info, file)

# json.load()函数的使用,将读取json信息
file = open('1.json', 'r', encoding='utf-8')
info = json.load(file)
print(info)
json序列化类对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import json

class Student(object):
def __init__(self, name, age, score):
self.name = name
self.age = age
self.score = score

#######################################
# 序列化对象:dumps接收自定义转换函数
#######################################

s = Student('Bob', 20, 88)

# 定义函数将对象转为dict
def student2dict(std):
return {
'name': std.name,
'age': std.age,
'score': std.score
}

# dumps()方法可选参数default用来指定转换方法
print(json.dumps(s, default=student2dict))

# 把任意class的实例变为dict
# class的实例都有一个__dict__属性,但是定义了__slots__的class没有;
print(json.dumps(s, default=lambda objx: objx.__dict__))

#######################################
# 对象反序列化:loads接收自定义转换函数
#######################################

# 反序列化对象
def dict2student(d):
return Student(d['name'], d['age'], d['score'])

# 自定义转换函数
json_str = '{"age": 20, "score": 88, "name": "Bob"}'
print(json.loads(json_str, object_hook=dict2student))

obj = dict(name='小明', age=20)
# ensure_ascii: 表示是否使用ascii编码,ascii不能正确显示中文
s = json.dumps(obj)
print(s)

多任务

多任务的实现有3种方式:

  1. 多进程模式;
  2. 多线程模式;
  3. 多进程+多线程模式。

多进程

os模块进程方法

  • os.getppid():获取父进程id;
  • os.getpid():获取当前进程id;
  • os.fork():创建子进程(仅支持Unix/Linux/Mac);

fork进程

在Python的os模块的fork函数可以轻松创建子进程,但是fork()仅支持Unix/Linux/Mac;

调用fork函数创建进程,需要注意fork()调用一次,返回两次:

for()调用时操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回;

  • 子进程永远返回0;
  • 父进程返回子进程的ID。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os

print('Process (%s) start...' % os.getpid())

# 创建一个子进程
pid = os.fork()
if pid == 0:
print('子进程 (%s) 的父进程是 %s.' % (os.getpid(), os.getppid()))
else:
print('父进程 (%s) 创建了一个子进程 (%s).' % (os.getpid(), pid))

# Process c start...
# 子进程 c 的父进程是 p.
# 父进程 p 创建了一个子进程 c.

跨平台进程模块

multiprocessing模块就是跨平台版本的多进程模块.

multiprocessing模块提供了一个Process类来表示一个进程对象;

Process进程对象
构造方法
1
2
3
4
5
6
7
def __init__(self, group=None, target=None, name=None, args=(), kwargs={})
【参数解析】
- group:进程所属组,基本不用
- target:进程调用的处理方法对象(可以是一个函数名,也可以是一个可调用的对象(实现了__call__方法的类))
- args:调用对象的位置参数元组
- name:别名
- kwargs:调用对象的关键字参数字典
实例方法
1
2
3
4
5
is_alive():返回进程是否在运行 
start():启动进程,等待CPU调度
join([timeout]):阻塞当前上下文环境,直到调用此方法的进程终止或者到达指定timeout
terminate():不管任务是否完成,立即停止该进程
run():start()调用该方法,当实例进程没有传入target参数,stat()将执行默认的run()方法
实例属性
1
2
3
4
5
- authkey:进程的认证密钥(字节字符串) 
- daemon:守护进程标识,在start()调用之前可以对其进行修改
- exitcode:进程的退出状态码
- name:进程名
- pid:进程
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from multiprocessing import Process
import os

# 定义函数,作为进程任务
def run_proc(name):
print('进程(%s)运行中: 接收外部信息参数name = %s' % (os.getpid(),name))

if __name__ == '__main__':
print('当前进程是 %s.' % os.getpid())
# 1. 创建一个进程
p = Process(target=run_proc, args=('test',))
print('子进程将开始...')
# 2. 启动进程
p.start()
# 3. join方法阻塞进程,等待进程结束再继续执行
p.join()
print('子进程结束')
Pool进程池

如果要启动大量的子进程,可以用进程池Pool的方式批量创建子进程;

要点
  • Pool的默认大小是CPU的核数;
Pool实例方法
  • 创建进程池:Pool(count);
  • 创建子进程:
1
2
3
4
5
6
7
# 创建子进程任务,串行执行(需要等待当前子进程执行完毕后,再执行下一个进程);
def apply(self, func, args=(), kwds={}):
pass

# 创建子进程任务,并行执行(不用等待当前进程执行完毕,随时根据系统调度来进行进程切换);
def apply_async(self, func, args=(), kwds={}, callback=None,error_callback=None):
pass
  • close():关闭pool,使其不在接受新的任务;
  • terminate():结束工作进程,不在处理未完成的任务。
  • join():主进程阻塞,等待子进程的退出,join方法要在close或terminate之后使用。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from multiprocessing import Pool
import os, time, random

# 用于创建子进程的耗时任务函数
def long_time_task(name):
print('Run task %s (%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('Task %s runs %0.2f seconds.' % (name, (end - start)))

if __name__ == '__main__':
print('Parent process %s.' % os.getpid())
p = Pool(4)
for i in range(5):
# 异步执行一个进程任务,并行任务
# 异步任务进程
p.apply_async(long_time_task, args=(i,))
# 同步任务进程
# p.apply(long_time_task, args=(i,))
print('Waiting for all subprocesses done...')
# 调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了;
p.close()
# join()方法会等待所有子进程执行完毕;
p.join()
print('All subprocesses done.')
subprocess子进程模块

subprocess模块用来创建子进程;

subprocess模块允许创建一个新的进程来执行另外的程序,可以与进程进行通信,获取标准的输入、标准输出、标准错误以及返回码等。

subprocess模块常用函数
  • 常用函数表:
函数 描述
run() Python 3.5中新增的函数。执行指定的命令,等待命令执行完成后返回一个包含执行结果的CompletedProcess类的实例。
call() 执行指定的命令,返回命令执行状态,其功能类似于os.system(cmd)。
check_call() Python 2.5中新增的函数。 执行指定的命令,如果执行成功则返回状态码,否则抛出异常。其功能等价于subprocess.run(…, check=True)。
check_output() Python 2.7中新增的的函数。执行指定的命令,如果执行状态码为0则返回命令执行结果,否则抛出异常。
getoutput(cmd) 接收字符串格式的命令,执行命令并返回执行结果,其功能类似于os.popen(cmd).read()和commands.getoutput(cmd)。
getstatusoutput(cmd) 执行cmd命令,返回一个元组(命令执行状态, 命令执行结果输出),其功能类似于commands.getstatusoutput()。

表中常用函数都是基于subprocess.Popen类实现的。

1
2
3
4
5
【说明】
1. 在Python 3.5之后的版本中,官方文档中提倡通过subprocess.run()函数替代其他函数来使用subproccess模块的功能;
2. 在Python 3.5之前的版本中,我们可以通过subprocess.call(),subprocess.getoutput()等上面列出的其他函数来使用subprocess模块的功能;
3. subprocess.run()、subprocess.call()、subprocess.check_call()和subprocess.check_output()都是通过对subprocess.Popen的封装来实现的高级函数,因此如果我们需要更复杂功能时,可以通过subprocess.Popen来完成。
4. subprocess.getoutput()和subprocess.getstatusoutput()函数是来自Python 2.x的commands模块的两个遗留函数。它们隐式的调用系统shell,并且不保证其他函数所具有的安全性和异常处理的一致性。另外,它们从Python 3.3.4开始才支持Windows平台。
  • 常用函数的定义:
1
2
3
subprocess.call(*popenargs, *, stdin=None, stdout=None, stderr=None, shell=False, timeout=None)
subprocess.run(*popenargs,input=None, capture_output=False, timeout=None, check=False, **kwargs)
//...
1
2
3
4
5
6
7
8
9
【参数说明】
- args:要执行的shell命令,默认应该是一个字符串序列,如['df', '-Th']或('df', '-Th'),也可以是一个字符串,如'df -Th',但是此时需要把shell参数的值置为True。
- shell: 如果shell为True,那么指定的命令将通过shell执行。如果我们需要访问某些shell的特性,如管道、文件名通配符、环境变量扩展功能,这将是非常有用的。当然,python本身也提供了许多类似shell的特性的实现,如glob、fnmatch、os.walk()、os.path.expandvars()、os.expanduser()和shutil等。
- check: 如果check参数的值是True,且执行命令的进程以非0状态码退出,则会抛出一个CalledProcessError的异常,且该异常对象会包含 参数、退出状态码、以及stdout和stderr(如果它们有被捕获的话)。
- stdout, stderr:run()函数默认不会捕获命令执行结果的正常输出和错误输出,如果我们向获取这些内容需要传递subprocess.PIPE,然后可以通过返回的CompletedProcess类实例的stdout和stderr属性或捕获相应的内容;
call()和check_call()函数返回的是命令执行的状态码,而不是CompletedProcess类实例,所以对于它们而言,stdout和stderr不适合赋值为subprocess.PIPE;
- check_output()函数默认就会返回命令执行结果,所以不用设置stdout的值,如果我们希望在结果中捕获错误信息,可以执行stderr=subprocess.STDOUT。
- input:该参数是传递给Popen.communicate(),通常该参数的值必须是一个字节序列,如果universal_newlines=True,则其值应该是一个字符串。
universal_newlines:该参数影响的是输入与输出的数据格式,比如它的值默认为False,此时stdout和stderr的输出是字节序列;当该参数的值设置为True时,stdout和stderr的输出是字符串。
  • 示例:
1
2
3
4
5
import subprocess

# 执行指定的命令,返回命令执行状态,其功能类似于os.system(cmd)。
r = subprocess.call(['nslookup', 'www.python.org'])
print('result:', r)
subproces.Popen()

如果我们需要更复杂功能时,并且要与进程进行复杂的交互,可以利用Popen类来实现;

1> 构造函数概述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 构造函数
class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=False,
startup_info=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=())

# 参数说明
> args:要执行的shell命令,可以是字符串,也可以是命令各个参数组成的序列。当该参数的值是一个字符串时,该命令的解释过程是与平台相关的,因此通常建议将args参数作为一个序列传递。
> bufsize:指定缓存策略,0表示不缓冲,1表示行缓冲,其他大于1的数字表示缓冲区大小,负数 表示使用系统默认缓冲策略。
> stdin, stdout, stderr:分别表示程序标准输入、输出、错误句柄。
> preexec_fn:用于指定一个将在子进程运行之前被调用的可执行对象,只在Unix平台下有效。
> close_fds:如果该参数的值为True,则除了0,12之外的所有文件描述符都将会在子进程执行之前被关闭。
> shell:该参数用于标识是否使用shell作为要执行的程序,如果shell值为True,则建议将args参数作为一个字符串传递而不要作为一个序列传递。
> cwd:如果该参数值不是None,则该函数将会在执行这个子进程之前改变当前工作目录。
> env:用于指定子进程的环境变量,如果env=None,那么子进程的环境变量将从父进程中继承。如果env!=None,它的值必须是一个映射对象。
> universal_newlines:如果该参数值为True,则该文件对象的stdin,stdout和stderr将会作为文本流被打开,否则他们将会被作为二进制流被打开。
> startupinfo和creationflags: 这两个参数只在Windows下有效,它们将被传递给底层的CreateProcess()函数,用于设置子进程的一些属性,如主窗口的外观,进程优先级等。

注意:如果希望通过进程的stdin向其发送数据,在创建Popen对象的时候,参数stdin必须被设置为PIPE。同样,如果希望从stdout和stderr获取数据,必须将stdout和stderr设置为PIPE。

2> Popen实例方法:

方法 描述
poll() 用于检查子进程(命令)是否已经执行结束,没结束返回None,结束后返回状态码。
wait(timeout=None) 等待子进程结束,并返回状态码;如果在timeout指定的秒数之后进程还没有结束,将会抛出一个TimeoutExpired异常。
communicate() 和子进程交互(发送数据到stdin,并从stdout和stderr读数据,直到收到EOF,要给子进程的stdin发送数据,则Popen时要设置stdin要为PIPE)。等待子进程结束。
send_signal(signal) 发送指定的信号给这个子进程。
terminate() 停止该子进程。
kill() 杀死该子进程。

3> Popen实例属性:

1
2
3
4
5
6
7
8
9
> p.pid:  子进程的PID。
> p.returncode: 该属性表示子进程的返回状态,returncode可能有多重情况(
None 子进程尚未结束,
==0 子进程正常退出;
> 0 子进程异常退出,
returncode对应于出错码;
< 0—— 子进程被信号杀掉了。
)
> p.stdin, p.stdout, p.stderr: 子进程对应的一些初始文件,如果调用Popen()的时候对应的参数是subprocess.PIPE,则这里对应的属性是一个包裹了这个管道的 file 对象;

4> 示例:

1
2
3
4
5
6
7
8
import subprocess

cmd = 'adb devices' # 获取连接设备
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
print(str(p.stdout.read(), encoding='utf-8')) # 打印结果
# 阻塞子进程,直到子进程结束。
p.communicate()
print("end")
进程间通信

Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。

父进程所有Python对象都必须通过pickle序列化再传到子进程去,所有,如果multiprocessing在Windows下调用失败了,要先考虑是不是pickle失败了。

Queue

在python中,多个线程之间的数据是共享的,多个线程进行数据交换的时候,不能够保证数据的安全性和一致性,所以当多个线程需要进行数据交换的时候,队列就出现了,队列可以完美解决线程间的数据交换,保证线程间数据的安全性和一致性。

三种队列:

  • Python queue模块的FIFO队列先进先出。 class queue.Queue(maxsize)
  • LIFO类似于堆,即先进后出。 class queue.LifoQueue(maxsize)
  • 还有一种是优先级队列级别越低越先出来。 class queue.PriorityQueue(maxsize)

常用方法:

  • queue.qsize() 返回队列的大小
  • queue.empty() 如果队列为空,返回True,反之False
  • queue.full() 如果队列满了,返回True,反之False
  • queue.full 与 maxsize 大小对应
  • queue.get([block[, timeout]])获取队列,timeout等待时间(从队列中移除并返回一个数据,通知q可以继续添加数据)
  • queue.get_nowait() 相当queue.get(False)
  • queue.put(item) 写入队列,timeout等待时间(默认的情况,阻塞调用,等待q.get移除数据后继续执行)
  • queue.put_nowait(item) 相当queue.put(item, False)
  • queue.task_done() 在完成一项工作之后,queue.task_done()函数向任务已经完成的队列发送一个信号
  • queue.join() 实际上意味着等到队列为空,再执行别的操作

示例:
在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from multiprocessing import Process, Queue
import os, time, random

# 写数据进程执行的代码:
def write(q):
print('Process to write: %s' % os.getpid())
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
# 默认的情况,阻塞调用,等待q.get移除数据后继续执行;
q.put(value)
time.sleep(random.random())

# 读数据进程执行的代码:
def read(q):
print('Process to read: %s' % os.getpid())
while True:
# 从队列中移除并返回一个数据,通知q可以继续添加数据
value = q.get(True)
print('Get %s from queue.' % value)

if __name__=='__main__':
# 父进程创建Queue,并传给各个子进程:
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 启动子进程pw,写入:
pw.start()
# 启动子进程pr,读取:
pr.start()
# 等待pw结束:
pw.join()
# pr进程里是死循环,无法等待其结束,只能在pw结束时强行终止:
pr.terminate()
多进程的优缺点
优点
  • 多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。
缺点
  • 多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。

多线程

threading模块

Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

线程概念
  • 由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程;
  • Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。
  • 主线程实例的名字叫MainThread,子线程的名字在创建时指定,如果不起名字Python就自动给线程命名为Thread-1,Thread-2等。
缺点
  • 多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。
创建线程实例

启动一个线程:创建Thread实例,需要传入一个处理函数传入,然后调用start()开始执行;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import time, threading


# 新线程执行的代码:
def loop():
print('thread %s is running...' % threading.current_thread().name)
n = 0
while n < 5:
n = n + 1
print('thread %s >>> %s' % (threading.current_thread().name, n))
time.sleep(1)
print('thread %s ended.' % threading.current_thread().name)


print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)

Lock(锁)

锁用来解决多个线程间数据共享导致的数据修改错误;

数据共享问题
  • 多进程中,同一个变量,互不影响;
  • 多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
使用锁

多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。

  • 多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。
  • 获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try…finally来确保锁一定会被释放。
  • 锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import time, threading

# 假定这是你的银行存款:
balance = 0
# 创建一个锁
lock = threading.Lock()

def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n

def run_thread(n):
for i in range(100000):
# 先要获取锁,给change_it()上一把锁,其它线程不能访问该方法
lock.acquire()
try:
change_it(n)
finally:
lock.release()

t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
# result: 0

GIL全局锁

1.现象描述?

理论上来看一个死循环线程会100%占用一个CPU,用C、C++或Java在n个线程中写相同的死循环,直接可以把n核心跑满,4核就跑到400%,8核就跑到800%,但是Python不会占满n核CPU;

2.python的GIL锁;

Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

3.利用多核CPU:

  • 通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
  • 在Python中,可以使用多线程,但不要指望能有效利用多核。
  • 如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。

Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。

ThreadLocal

ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。

在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁,但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦,ThreadLocal类型的全局变量,可以绑定其他变量,并且可以在多个线程中使用它,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。

  • 一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。
  • ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
示例
  1. 创建ThreadLocal全局变量,通过绑定其它变量可以在多个线程中互不影响的特性,解决参数在一个线程中各个函数之间互相传递的问题;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import threading

# 创建全局ThreadLocal对象:
local_school = threading.local()

def process_student():
# 获取当前线程关联的student:
std = local_school.student
print('Hello, %s (in %s)' % (std, threading.current_thread().name))

def process_thread(name):
# 绑定ThreadLocal的student:
local_school.student = name
process_student()

t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

分布式进程

在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。

Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。Python的分布式进程接口简单,封装良好,适合需要把繁重任务分布到多台机器的环境下。

例子

需要在linux/mac系统下,windows无法正常运行;

如果我们已经有一个通过Queue通信的多进程程序在同一台机器上运行,现在,由于处理任务的进程任务繁重,希望把发送任务的进程和处理任务的进程分布到两台机器上。怎么用分布式进程实现?

原有的Queue可以继续使用,但是,通过managers模块把Queue通过网络暴露出去,就可以让其他机器的进程访问Queue了。

  1. 我们先看服务进程,服务进程负责启动Queue,把Queue注册到网络上,然后往Queue里面写入任务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# task_master.py

import random, time, queue
from multiprocessing.managers import BaseManager

# 发送任务的队列:
task_queue = queue.Queue()
# 接收结果的队列:
result_queue = queue.Queue()

# 从BaseManager继承的QueueManager:
class QueueManager(BaseManager):
pass

# 把两个Queue都注册到网络上, callable参数关联了Queue对象:
QueueManager.register('get_task_queue', callable=lambda: task_queue)
QueueManager.register('get_result_queue', callable=lambda: result_queue)
# 绑定端口5000, 设置验证码'abc':
manager = QueueManager(address=('', 5000), authkey=b'abc')
# 启动Queue:
manager.start()
# 获得通过网络访问的Queue对象:
task = manager.get_task_queue()
result = manager.get_result_queue()
# 放几个任务进去:
for i in range(10):
n = random.randint(0, 10000)
print('Put task %d...' % n)
task.put(n)
# 从result队列读取结果:
print('Try get results...')
for i in range(10):
r = result.get(timeout=10)
print('Result: %s' % r)
# 关闭:
manager.shutdown()
print('master exit.')
1
请注意,当我们在一台机器上写多进程程序时,创建的Queue可以直接拿来用,但是,在分布式多进程环境下,添加任务到Queue不可以直接对原始的task_queue进行操作,那样就绕过了QueueManager的封装,必须通过manager.get_task_queue()获得的Queue接口添加。
  1. 然后,在另一台机器上启动任务进程(本机上启动也可以):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# task_worker.py

import time, sys, queue
from multiprocessing.managers import BaseManager
from multiprocessing import Queue

# 创建类似的QueueManager:
class QueueManager(BaseManager):
pass

# 由于这个QueueManager只从网络上获取Queue,所以注册时只提供名字:
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')

# 连接到服务器,也就是运行task_master.py的机器:
server_addr = '127.0.0.1'
print('Connect to server %s...' % server_addr)
# 端口和验证码注意保持与task_master.py设置的完全一致:
m = QueueManager(address=(server_addr, 5000), authkey=b'abc')
# 从网络连接:
m.connect()
# 获取Queue的对象:
task = m.get_task_queue()
result = m.get_result_queue()
# 从task队列取任务,并把结果写入result队列:
for i in range(10):
try:
n = task.get(timeout=1)
print('run task %d * %d...' % (n, n))
r = '%d * %d = %d' % (n, n, n*n)
time.sleep(1)
result.put(r)
except Queue.Empty:
print('task queue is empty.')
# 处理结束:
print('worker exit.')
  1. 运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
先启动task_master.py服务进程:
$ python3 task_master.py
Put task 3411...
Put task 1605...
Put task 1398...
Put task 4729...
Put task 5300...
Put task 7471...
Put task 68...
Put task 4219...
Put task 339...
Put task 7866...
Try get results...

task_master.py进程发送完任务后,开始等待result队列的结果。现在启动task_worker.py进程:
$ python3 task_worker.py
Connect to server 127.0.0.1...
run task 3411 * 3411...
run task 1605 * 1605...
run task 1398 * 1398...
run task 4729 * 4729...
run task 5300 * 5300...
run task 7471 * 7471...
run task 68 * 68...
run task 4219 * 4219...
run task 339 * 339...
run task 7866 * 7866...
worker exit.

task_worker.py进程结束,在task_master.py进程中会继续打印出结果:
Result: 3411 * 3411 = 11634921
Result: 1605 * 1605 = 2576025
Result: 1398 * 1398 = 1954404
Result: 4729 * 4729 = 22363441
Result: 5300 * 5300 = 28090000
Result: 7471 * 7471 = 55815841
Result: 68 * 68 = 4624
Result: 4219 * 4219 = 17799961
Result: 339 * 339 = 114921
Result: 7866 * 7866 = 61873956

常用内建模块

datetime

datetime是Python处理日期和时间的标准库,主要使用datetime模块中的datatime类来获取时间。

1
2
# 导入datetime类
from datetime import datetime
本地时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from datetime import datetime

# 获取当前datetime
now = datetime.now()
print(type(now)) # 2018-11-07 16:12:09.036856

# 用指定日期时间创建datetime
dt = datetime(2015, 4, 19, 12, 20)
print(dt) # 2015-04-19 12:20:00

# 获取单位时间
# 年
print(datetime.now().year)
# 月
print(datetime.now().month)
# 日
print(datetime.now().day)
# 时
print(datetime.now().hour)
# 分
print(datetime.now().minute)
# 秒
print(datetime.now().second)
时间加减法

timedelta对象表示一个时间长度,用来计算两个日期或者时间的差值;

1
2
3
4
5
6
7
from datetime import datetime, timedelta

now = datetime.now()
# 当前时间减2小时
print(now + timedelta(hours=2))
# 当前时间加2天2小时
print(now - timedelta(days=2, hours=2))
时区时间

UTC时间是格林威治标准时间,比北京时间早了8小时。

创建时区时间
  • 本地时间是指系统设定时区的时间,例如北京时间是UTC+8:00时区的时间,而UTC时间指UTC+0:00时区的时间。
  • datetime类型有一个时区属性tzinfo,但是默认为None,所以无法区分这个datetime到底是哪个时区,除非强行给datetime设置一个时区;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from datetime import datetime, timedelta, timezone

# 获取一个标准UTC时间
print(datetime.utcnow())
# 获取UTC时间,并强制设置时区为UTC+0:00:
utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc)
print(utc_dt)

# 直接获取一个标准UTC+0:00时间
print(datetime.now(tz=timezone.utc))

# 创建时区UTC+8:00 北京时间
tz_utc_8 = timezone(timedelta(hours=8))
# 强制设置为UTC+8:00
print(datetime.now().replace(tzinfo=tz_utc_8))
时区转换

可以先通过utcnow()拿到当前的UTC时间,再通过astimezone根据时间差转换为任意时区的时间;

1
2
3
4
5
6
7
8
9
10
11
12
13
from datetime import datetime, timedelta, timezone

# 拿到UTC时间,并强制设置时区为UTC+0:00:
utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc)
print(utc_dt)

# astimezone()将转换时区为北京时间:
bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))
print(bj_dt)

# astimezone()将转换时区为东京时间:
tokyo_dt = utc_dt.astimezone(timezone(timedelta(hours=9)))
print(tokyo_dt)
时间戳

Python的timestamp是一个浮点数。如果有小数位,小数位表示毫秒数。

1
2
3
4
5
6
7
8
9
10
11
from datetime import datetime

# datetime转timestamp
print(datetime.now().timestamp())

# timestamp转datetime
t = datetime.now().timestamp()
print(datetime.fromtimestamp(t))

# timestamp转UTC时间
print(datetime.utcfromtimestamp(t))
时间格式化

Python time strftime() 函数接收以时间元组,并返回以可读字符串表示的当地时间,格式由参数format决定。

语法
1
2
3
4
time.strftime(format[, t])
【参数】
format -- 格式字符串。
t -- 可选的参数t是一个struct_time对象。
时间日期格式化符号

python中时间日期格式化符号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
%y 两位数的年份表示(00-99)
%Y 四位数的年份表示(000-9999)
%m 月份(01-12)
%d 月内中的一天(0-31)
%H 24小时制小时数(0-23)
%I 12小时制小时数(01-12)
%M 分钟数(00=59)
%S 秒(00-59)
%a 本地简化星期名称
%A 本地完整星期名称
%b 本地简化的月份名称
%B 本地完整的月份名称
%c 本地相应的日期表示和时间表示
%j 年内的一天(001-366)
%p 本地A.M.或P.M.的等价符
%U 一年中的星期数(00-53)星期天为星期的开始
%w 星期(0-6),星期天为星期的开始
%W 一年中的星期数(00-53)星期一为星期的开始
%x 本地相应的日期表示
%X 本地相应的时间表示
%Z 当前时区的名称
%% %号本身
示例
1
2
3
4
from datetime import datetime

# datetime转换为str(格式化时间)
print(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))

collections

collections是Python内建的一个集合模块,提供了许多有用的集合类。

namedtuple

利用元组创建对象

namedtuple是一个函数,它用来创建一个自定义的tuple对象,并且规定了tuple元素的个数,并可以用属性而不是索引来引用tuple的某个元素。

语法
1
namedtuple('名称', [属性list]):
示例
1
2
3
4
5
6
7
8
9
10
11
12
from collections import namedtuple

# 例1:定义一个点对象,拥有两个坐标点
Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
print(p.x, p.y)

print(isinstance(p, Point))
print(isinstance(p, tuple))

# 例2:用坐标和半径表示一个圆
Circle = namedtuple('Circle', ['x', 'y', 'r'])
deque

加强版list

使用list存储数据时,按索引访问元素很快,但是插入和删除元素就很慢了,因为list是线性存储,数据量大的时候,插入和删除效率很低。

deque是为了高效实现插入和删除操作的双向列表,适合用于队列和栈:

  • deque除了实现list的append()和pop()外,还支持appendleft()和popleft(),这样就可以非常高效地往头部添加或删除元素。
1
2
3
4
5
6
7
from collections import deque

q = deque(['a', 'b', 'c'])
q.remove('a')
q.append('x')
q.appendleft('y')
print(q) # deque(['y', 'b', 'c', 'x'])
defaultdict

dict设置默认值

使用dict时,当通过dict.[key]访问value时,如果引用的Key不存在,就会报错KeyError。如果希望key不存在时,返回一个默认值,就可以用defaultdict;

  • 注意默认值是调用函数返回的,而函数在创建defaultdict对象时传入。
  • 除了在Key不存在时返回默认值,defaultdict的其他行为跟dict是完全一样的。
1
2
3
4
from collections import defaultdict

dd = defaultdict(lambda: 'N/A')
print(dd['key'])
OrderedDict

dict的key支持按顺序迭代

使用dict时,Key是无序的。在对dict做迭代时,我们无法确定Key的顺序。如果要保持Key的顺序,可以用OrderedDict;

  • OrderedDict的Key会按照插入的顺序排列,不是Key本身排序;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from collections import OrderedDict

d_t = dict()
print(d_t)

# 创建字典:无序的
d = dict([('a', 1), ('b', 2), ('c', 3)])
print(d)

# 有序字典
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
print(od)

# 按照插入的Key的顺序返回
od = OrderedDict()
od['z'] = 1
od['y'] = 2
od['x'] = 3
print(list(od.keys()))
实现FIFO的dict

OrderedDict可以实现一个FIFO(先进先出)的dict,当容量超出限制时,先删除最早添加的Key;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from collections import OrderedDict

class LastUpdatedOrderedDict(OrderedDict):

def __init__(self, capacity):
super(LastUpdatedOrderedDict, self).__init__()
self._capacity = capacity

def __setitem__(self, key, value):
containsKey = 1 if key in self else 0
if len(self) - containsKey >= self._capacity:
last = self.popitem(last=False)
print('remove:', last)
if containsKey:
del self[key]
print('set:', (key, value))
else:
print('add:', (key, value))
OrderedDict.__setitem__(self, key, value)

d = LastUpdatedOrderedDict(2)
d['key1'] = 1
d['key2'] = 2
d['key3'] = 3
d['key4'] = 4
print(d)
ChainMap

让参数按定义的顺序查找;

ChainMap可以把一组dict串起来组成一个逻辑上的dict。ChainMap本身也是一个dict,但是查找的时候,会按照顺序在内部的dict依次查找。

使用场景示例

什么时候使用ChainMap最合适?举个例子:应用程序往往都需要传入参数,参数可以通过命令行传入,可以通过环境变量传入,还可以有默认参数。我们可以用ChainMap实现参数的优先级查找,即先查命令行参数,如果没有传入,再查环境变量,如果没有,就使用默认参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from collections import ChainMap
# python中的命令行解析最简单最原始的方法是使用sys.argv来实现,更高级的可以使用argparse这个模块。
import os, argparse

# 构造缺省参数:
defaults = {
'color': 'red',
'user': 'guest'
}

# 构造命令行参数:
parser = argparse.ArgumentParser()
# 指定程序需要接受的命令参数
parser.add_argument('-u', '--user')
parser.add_argument('-c', '--color')
# 将之前add_argument()定义的参数进行赋值,并返回相关的namespace
namespace = parser.parse_args()
command_line_args = {k: v for k, v in vars(namespace).items() if v}

# 组合成ChainMap:
combined = ChainMap(command_line_args, os.environ, defaults)

# 没有任何参数时,打印出默认参数:
print('color=%s' % combined['color'])
print('user=%s' % combined['user'])
Counter

Counter是一个简单的计数器,Counter实际上也是dict的一个子类;

示例:统计字符出现的个数?

1
2
3
4
5
6
7
8
from collections import Counter

c = Counter()

for ch in 'programming':
c[ch] = c[ch] + 1

print(c)

base64

Base64是一种用64个字符来表示任意二进制数据的方法,Base64是一种最常见的二进制编码方法。

base64编解码
1
2
3
4
5
6
7
import base64

# base64编码
encodeResult = base64.b64encode(b'10000011')
print(encodeResult)
# base64解码
print(base64.b64decode(encodeResult))
urlsafe_b64编解码

urlsafe_b64编码后把字符+和/分别变成-和_;

1
2
3
4
5
6
7
8
9
10
11
import base64

test_str = b'i\xb7\x1d\xfb\xef\xff'

# base64编码
print(base64.b64encode(test_str))
# urlsafe_b64编码(标准的Base64编码后可能出现字符+和/,在URL中就不能直接作为参数,所以又有一种"url safe"的base64编码,其实就是把字符+和/分别变成-和_)
safe_encode = base64.urlsafe_b64encode(test_str)
print(safe_encode)
# urlsafe_b64解码
print(base64.urlsafe_b64decode(safe_encode))
要点
  • Base64是一种通过查表的编码方法,不能用于加密,即使使用自定义的编码表也不行。
  • Base64适用于小段内容的编码,比如数字证书签名、Cookie的内容等。
  • 由于=字符也可能出现在Base64编码中,但=用在URL、Cookie里面会造成歧义,所以,很多Base64编码后会把=去掉;
  • Base64是把3个字节变为4个字节,所以,Base64编码的长度永远是4的倍数,因此,需要加上=把Base64字符串的长度变为4的倍数,就可以正常解码了;

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#【解码byte非4的倍数用等号补位】
import base64

# 能处理掉“=”的base64解码函数
def safe_base64_decode(s):
# 添加等于号
if len(s) % 4 != 0:
s = s + bytes('=', encoding='utf-8') * (4 - len(s) % 4)
print("1:%s" % s)
# 解决字符串和bytes类型
if not isinstance(s, bytes):
s = bytes(s, encoding='utf-8')
print('s:%s' % s)
# 解码
base64_string = base64.b64decode(s)
print("base64:%s" % base64_string)
return base64_string


# 测试:
# assert b'abcd' == safe_base64_decode(b'YWJjZA=='), safe_base64_decode('YWJjZA==')
# assert b'abcd' == safe_base64_decode(b'YWJjZA'), safe_base64_decode('YWJjZA')
# print('ok')

# 解码带等号=的bytes
safe_base64_decode(b'YWJjZA==')

# 字符串转byte再解码
safe_base64_decode('YWJjZA==')

# 编码长度不是4的倍数时,用等号补位;
safe_base64_decode(b'YWJjZA')

struct

Python提供了一个struct模块来解决bytes和其它数据类型的转换;

根据格式化字符串(参考struct官方文档)的格式来进行bytes和其它数据类型的转换;

主要函数

struct模块中最主要的三个函数式pack()、unpack()、calcsize(),语法如下:

1
2
3
pack(fmt, v1, v2, ...)  ------ 根据所给的fmt指令描述的格式将值v1,v2,...转换为一个字符串,后面参数个数要和处理指令一致。
unpack(fmt, bytes) ------ 根据所给的fmt指令描述的格式将bytes反向解析出来,返回一个元组。
calcsize(fmt) ------ 根据所给的fmt描述的格式返回该结构的大小。

fmt由字节顺序符和格式字符组成(参考struct官方文档);

示例
1
2
3
4
5
6
7
8
import struct

# pack把任意数据类型变成bytes(>表示字节顺序是big-endian,也就是网络序,I表示4字节无符号整数。)
print(struct.pack('>I', 10240099)) # b'\x00\x9c@c'

# unpack把bytes变成相应的数据类型(>表示字节顺序是big-endian,为I:4字节无符号整数和H:2字节无符号整数。)
# 此处解释为:字节依次将前四个字节、后两个字节转换成无符号整数;
print(struct.unpack('>IH', b'\xf0\xf0\xf0\xf0\x80\x80')) # (4042322160, 32896)

hashlib

Python的hashlib提供了常见的摘要算法,如MD5,SHA1等等。

摘要算法

任何摘要算法都是把无限多的数据集合映射到一个有限的集合中, 这种碰撞有可能逆推出原文;

  • 摘要算法又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示)。
  • 摘要算法就是通过摘要函数f()对任意长度的数据data计算出固定长度的摘要digest,目的是为了发现原始数据是否被人篡改过。
  • 摘要算法之所以能指出数据是否被篡改过,就是因为摘要函数是一个单向函数,计算f(data)很容易,但通过digest反推data却非常困难。而且,对原始数据做一个bit的修改,都会导致计算出的摘要完全不同。
  • 摘要算法在很多地方都有广泛的应用。要注意摘要算法不是加密算法,不能用于加密(因为无法通过摘要反推明文),只能用于防篡改,但是它的单向计算特性决定了可以在不存储明文口令的情况下验证用户口令。
MD5

MD5是最常见的摘要算法,速度很快,生成结果是固定的128 bit字节,通常用一个32位的16进制字符串表示。

1
2
3
4
5
6
7
8
9
10
11
12
import hashlib

# 简单用法
md5 = hashlib.md5()
md5.update('how to use md5 in python hashlib?'.encode('utf-8'))
print(md5.hexdigest())

# 数据量很大,分块多次调用
md5 = hashlib.md5()
md5.update('how to use md5 in '.encode('utf-8'))
md5.update('python hashlib?'.encode('utf-8'))
print(md5.hexdigest())
SHA1
  • SHA1的结果是160 bit字节,通常用一个40位的16进制字符串表示。
  • 比SHA1更安全的算法是SHA256和SHA512,不过越安全的算法不仅越慢,而且摘要长度更长。
1
2
3
4
5
6
import hashlib

sha1 = hashlib.sha1()
sha1.update('how to use sha1 in '.encode('utf-8'))
sha1.update('python hashlib?'.encode('utf-8'))
print(sha1.hexdigest())

hmac

Python内置的hmac模块实现了标准的Hmac算法,它利用一个key对message计算“杂凑”后的hash,使用hmac算法比标准hash算法更安全,因为针对相同的message,不同的key会产生不同的hash。

解决问题场景

问题

通过哈希算法,我们可以验证一段数据是否有效,方法就是对比该数据的哈希值,例如,判断用户口令是否正确,我们用保存在数据库中的password_md5对比计算md5(password)的结果,如果一致,用户输入的口令就是正确的。

解决思路

为了防止黑客通过彩虹表根据哈希值反推原始口令,在计算哈希的时候,不能仅针对原始输入计算,需要增加一个salt来使得相同的输入也能得到不同的哈希,这样,大大增加了黑客破解的难度。

hmac算法

和我们自定义的加salt算法不同,Hmac算法针对所有哈希算法都通用,无论是MD5还是SHA-1。采用Hmac替代我们自己的salt算法,可以使程序算法更标准化,也更安全。

1
2
3
4
5
6
7
import hmac

message = b'Hello, world!'
key = b'secret'
h = hmac.new(key, message, digestmod='MD5')
# 如果消息很长,可以多次调用h.update(msg)
print(h.hexdigest())

可见使用hmac和普通hash算法非常类似。hmac输出的长度和原始哈希算法的长度一致。需要注意传入的key和message都是bytes类型,str类型需要首先编码为bytes。

itertools

  • Python的内建模块itertools提供了非常有用的用于操作迭代对象的函数。
  • itertools模块提供的全部是处理迭代功能的函数,它们的返回值不是list,而是Iterator,只有用for循环迭代的时候才真正计算。
无限迭代函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import itertools,time

# count(n)会创建一个自然数从n开始的无限迭代器,n如果不传,默认为0,n可以为负数;
natuals = itertools.count(1)
print(type(natuals))

for n in natuals:
print(n)
time.sleep(1)

# cycle(iterable)会将将传入的iterable按顺序无限迭代
cs = itertools.cycle('ABC')

for c in cs:
print(c)
time.sleep(1)

# repeat()负责把一个元素无限重复下去,不过如果提供第二个参数就可以限定重复次数:
ns = itertools.repeat('A', 3)
for n in ns:
print(n)
time.sleep(1)
多个迭代串联

chain()可以把一组迭代对象串联起来,形成一个更大的迭代器:

1
2
3
4
import itertools,time

for c in itertools.chain('ABC', 'XYZ'):
print(c)
重复元素分组迭代

groupby()把迭代器中相邻的重复元素挑出来放在一起:

1
2
3
4
5
6
7
8
9
import itertools,time

for key, group in itertools.groupby('AAABBBCCAAA'):
print(key, list(group))

print('------------------------------')
# 添加值处理函数,按照不区分大小写来挑选
for key, group in itertools.groupby('AaaBBbcCAAa', lambda c: c.upper()):
print(key, list(group))

contextlib

在Python中,读写文件这样的资源要特别注意,必须在使用完毕后正确关闭它们。可以使用try…finally,或者with;

with自动关闭资源

并不是只有open()函数返回的fp对象才能使用with语句。实际上,任何对象,只要正确实现了上下文管理,就可以用于with语句。

1
实现上下文管理是通过__enter__和__exit__这两个方法实现的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Query(object):

def __init__(self, name):
self.name = name

def __enter__(self):
print('Begin')
return self

def __exit__(self, exc_type, exc_value, traceback):
if exc_type:
print('Error')
else:
print('End')

def query(self):
print('Query info about %s...' % self.name)

with Query('Bob') as q:
q.query()
@contextmanager

上下文管理装饰器

@contextmanager让我们通过编写generator来简化上下文管理。

实现with
  1. @contextmanager这个decorator接受一个generator,用yield语句把with … as var把变量输出出去,然后,with语句就可以正常地工作了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from contextlib import contextmanager

class Query(object):

def __init__(self, name):
self.name = name

def query(self):
print('Query info about %s...' % self.name)

@contextmanager
def create_query(name):
print('Begin')
q = Query(name)
yield q
print('End')

with create_query('Bob') as q:
q.query()
  1. 希望在某段代码执行前后自动执行特定代码,也可以用@contextmanager实现:
1
2
3
4
5
6
7
8
9
10
11
from contextlib import contextmanager

@contextmanager
def tag(name):
print("<%s>" % name)
yield
print("</%s>" % name)

with tag("h1"):
print("hello")
print("world")
@closing

如果一个对象没有实现上下文,我们就不能把它用于with语句。这个时候,可以用closing()来把该对象变为上下文对象。

@closing的实现

closing也是一个经过@contextmanager装饰的generator,这个generator编写起来其实非常简单:

1
2
3
4
5
6
@contextmanager
def closing(thing):
try:
yield thing
finally:
thing.close()
使用案例

closing的作用就是把任意对象变为上下文对象,并支持with语句。

1
2
3
4
5
6
from contextlib import closing
from urllib.request import urlopen

with closing(urlopen('https://www.python.org')) as page:
for line in page:
print(line)

urllib

urllib提供了一系列用于操作URL的功能。

urllib提供的功能就是利用程序去执行各种HTTP请求。如果要模拟浏览器完成特定功能,需要把请求伪装成浏览器。伪装的方法是先监控浏览器发出的请求,再根据浏览器的请求头来伪装,User-Agent头就是用来标识浏览器的。

Get
抓取Get请求的信息

urllib的request模块可以非常方便地抓取URL内容,也就是发送一个GET请求到指定的页面,然后返回HTTP的响应;

例如,对豆瓣的一个URLhttps://api.douban.com/v2/book/2129650进行抓取,可以看到HTTP响应的头和JSON数据:

1
2
3
4
5
6
7
8
from urllib import request

with request.urlopen('https://api.douban.com/v2/book/2129650') as f:
data = f.read()
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', data.decode('utf-8'))
模拟浏览器发送GET请求

如果我们要想模拟浏览器发送GET请求,就需要使用Request对象,通过往Request对象添加HTTP头,我们就可以把请求伪装成浏览器。

例如,模拟iPhone 6去请求豆瓣首页,这样豆瓣会返回适合iPhone的移动版网页:

1
2
3
4
5
6
7
8
9
from urllib import request

req = request.Request('http://www.douban.com/')
req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
with request.urlopen(req) as f:
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', f.read().decode('utf-8'))
Post

如果要以POST发送一个请求,只需要把参数data以bytes形式传入。

案例:模拟一个微博登录,先读取登录的邮箱和口令,然后按照weibo.cn的登录页的格式以username=xxx&password=xxx的编码传入;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
print('Login to weibo.cn...')
email = input('Email: ')
passwd = input('Password: ')
login_data = parse.urlencode([
('username', email),
('password', passwd),
('entry', 'mweibo'),
('client_id', ''),
('savestate', '1'),
('ec', ''),
('pagerefer', 'https://passport.weibo.cn/signin/welcome?entry=mweibo&r=http%3A%2F%2Fm.weibo.cn%2F')
])

req = request.Request('https://passport.weibo.cn/sso/login')
req.add_header('Origin', 'https://passport.weibo.cn')
req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
req.add_header('Referer', 'https://passport.weibo.cn/signin/login?entry=mweibo&res=wel&wm=3349&r=http%3A%2F%2Fm.weibo.cn%2F')

with request.urlopen(req, data=login_data.encode('utf-8')) as f:
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', f.read().decode('utf-8'))
Handler

如果还需要更复杂的控制,比如通过一个Proxy去访问网站,我们需要利用ProxyHandler来处理,示例代码如下:

1
2
3
4
5
6
7
8
from urllib import request

proxy_handler = request.ProxyHandler({'http': 'http://www.example.com:3128/'})
proxy_auth_handler = request.ProxyBasicAuthHandler()
proxy_auth_handler.add_password('realm', 'host', 'username', 'password')
opener = request.build_opener(proxy_handler, proxy_auth_handler)
with opener.open('http://www.example.com/login.html') as f:
pass

XML

操作XML有两种方法:DOM和SAX。

  • DOM会把整个XML读入内存,解析为树,因此占用内存大,解析慢,优点是可以任意遍历树的节点。
  • SAX是流模式,边读边解析,占用内存小,解析快,缺点是我们需要自己处理事件。

正常情况下,优先考虑SAX,因为DOM实在太占内存。

SAX解析XML
  • SAX解析的关键函数:from xml.parsers.expat import ParserCreate;
  • 在Python中使用SAX解析XML非常简洁,通常我们关心的事件是start_element,end_element和char_data,准备好这3个函数,然后就可以解析xml了。
解析步骤

例如:解析一个html节点

1
<a href="/">python</a>
  1. 会产生3个事件:
1
2
3
start_element事件,在读取<a href="/">时;
char_data事件,在读取python时;
end_element事件,在读取</a>时。
  1. 定义实现了1中三个事件方法的类;
  2. 通过指定ParserCreate实例的三个属性与1中三个方法;
  3. 最后调用ParserCreate实例对象的Parse(xml)函数来解析;
案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from xml.parsers.expat import ParserCreate

class DefaultSaxHandler(object):
def start_element(self, name, attrs):
print('sax:start_element: %s, attrs: %s' % (name, str(attrs)))

def end_element(self, name):
print('sax:end_element: %s' % name)

def char_data(self, text):
print('sax:char_data: %s' % text)


xml = r'''<?xml version="1.0"?>
<ol>
<li><a href="/python">Python</a></li>
<li><a href="/ruby">Ruby</a></li>
</ol>
'''

handler = DefaultSaxHandler()
parser = ParserCreate()
parser.StartElementHandler = handler.start_element
parser.EndElementHandler = handler.end_element
parser.CharacterDataHandler = handler.char_data
parser.Parse(xml)
生成XML

除了解析XML外,如何生成XML呢?99%的情况下需要生成的XML结构都是非常简单的,因此,最简单也是最有效的生成XML的方法是拼接字符串;

1
2
3
4
5
6
L = []
L.append(r'<?xml version="1.0"?>')
L.append(r'<root>')
L.append(encode('some & data'))
L.append(r'</root>')
return ''.join(L)

HTMLParser

HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或SAX来解析HTML,Python提供了HTMLParser来非常方便地解析HTML。

  • feed()方法可以多次调用,也就是不一定一次把整个HTML字符串都塞进去,可以一部分一部分塞进去。
  • 特殊字符有两种,一种是英文表示的 ,一种是数字表示的Ӓ,这两种字符都可以通过Parser解析出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from html.parser import HTMLParser
from html.entities import name2codepoint

class MyHTMLParser(HTMLParser):

def error(self, message):
pass

def handle_starttag(self, tag, attrs):
print('<%s>' % tag)

def handle_endtag(self, tag):
print('</%s>' % tag)

def handle_startendtag(self, tag, attrs):
print('<%s/>' % tag)

def handle_data(self, data):
print(data)

def handle_comment(self, data):
print('<!--', data, '-->')

def handle_entityref(self, name):
print('&%s;' % name)

def handle_charref(self, name):
print('&#%s;' % name)


parser = MyHTMLParser()
parser.feed('''<html>
<head></head>
<body>
<!-- test html parser -->
<p>Some <a href=\"#\">html</a> HTML&nbsp;tutorial...<br>END</p>
</body></html>''')

常用第三方模块

除了内建的模块外,Python还有大量的第三方模块,所有的第三方模块都会在pypi上注册,只要找到对应的模块名字,即可用pip安装。

Pillow(图像处理)

Pillow是图像处理标准库,支持最新Python 3.x,详情见API文档;

安装
1
$ pip install pillow
操作图像

操作图像的缩放、切片、旋转、滤镜、输出文字、调色板等功能。

缩放
  • 打开一张图片并进行缩放:
1
2
3
4
5
6
7
8
9
10
11
12
13
from PIL import Image

# 打开一个jpg图像文件,注意是当前路径:
im = Image.open('talor_swift.jpg')
# 获得图像尺寸:
w, h = im.size
print('Original image size: %sx%s' % (w, h))
# 缩放到50%:
im.thumbnail((w//2, h//2))
print('Resize image to: %sx%s' % (w//2, h//2))
# 保存图片到本地save(self,filepath,format)
# 把缩放后的图像用jpeg格式保存:
im.save('thumbnail.jpg', 'jpeg')
模糊
  1. ImageFilter类中预定义了如下滤波方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
• BLUR:模糊滤波
• CONTOUR:轮廓滤波
• DETAIL:细节滤波
• EDGE_ENHANCE:边界增强滤波
• EDGE_ENHANCE_MORE:边界增强滤波(程度更深)
• EMBOSS:浮雕滤波
• FIND_EDGES:寻找边界滤波
• SMOOTH:平滑滤波
• SMOOTH_MORE:平滑滤波(程度更深)
• SHARPEN:锐化滤波
• GaussianBlur(radius=2):高斯模糊
>radius指定平滑半径。
• UnsharpMask(radius=2, percent=150, threshold=3):反锐化掩码滤波
>radius指定模糊半径;
>percent指定反锐化强度(百分比);
>threshold控制被锐化的最小亮度变化。
• Kernel(size, kernel, scale=None, offset=0):核滤波
当前版本只支持核大小为3x3和5x5的核大小,且图像格式为“L”和“RGB”的图像。
>size指定核大小(width, height);
>kernel指定核权值的序列;
>scale指定缩放因子;
>offset指定偏移量,如果使用,则将该值加到缩放后的结果上。
• RankFilter(size, rank):排序滤波
>size指定滤波核的大小;
>rank指定选取排在第rank位的像素,若大小为0,则为最小值滤波;若大小为size * size / 2则为中值滤波;若大小为size * size - 1则为最大值滤波。
• MedianFilter(size=3):中值滤波
>size指定核的大小
• MinFilter(size=3):最小值滤波器
>size指定核的大小
• MaxFilter(size=3):最大值滤波器
>size指定核的大小
• ModeFilter(size=3):波形滤波器
选取核内出现频次最高的像素值作为该点像素值,仅出现一次或两次的像素将被忽略,若没有像素出现两次以上,则保留原像素值。
>size指定核的大小;
  1. 示例:
1
2
3
4
5
6
7
8
from PIL import Image, ImageFilter

# 打开一个jpg图像文件,注意是当前路径:
im = Image.open('taylor_swift.jpg')
# 应用高斯模糊滤镜
im2 = im.filter(ImageFilter.GaussianBlur(radius=10))
# 保存图片到本地save(self,filepath,format)
im2.save('blur.jpg', 'jpeg')
绘图

PIL的ImageDraw提供了一系列绘图方法,让我们可以直接绘图。

生成字母验证码图片

用随机颜色填充背景,再画上文字,最后对图像进行模糊,得到验证码图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import random

# 随机字母:
def rnd_char():
return chr(random.randint(65, 90))

# 随机颜色1:
def rnd_color():
return random.randint(64, 255), random.randint(64, 255), random.randint(64, 255)

# 随机颜色2:
def rnd_color2():
return random.randint(32, 127), random.randint(32, 127), random.randint(32, 127)

# 设定验证码图片的宽高240x60:
width = 240
height = 60
# 创建图片
image = Image.new('RGB', (width, height), (122, 235, 66))
# 创建Font对象:
font = ImageFont.truetype('SF_Arch_Rival.ttf', 36)
# 创建Draw对象:
draw = ImageDraw.Draw(image)
# 填充每个像素的颜色:
for x in range(width):
for y in range(height):
draw.point((x, y), fill=rnd_color())
# 输出文字:
for t in range(4):
draw.text((60 * t + 10, 10), rnd_char(), font=font, fill=rnd_color2())
# 模糊:
image = image.filter(ImageFilter.BLUR)
image.save('code.jpg', 'jpeg')

requests(处理url)

处理URL资源比urllib更方便;

安装
1
$ pip install requests
GET请求
使用GET请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import requests

# GET请求访问豆瓣首页内容
r = requests.get('https://www.douban.com/')
# 获取请求的http状态码
print(r.status_code)
# 获取请求的url内容
print(r.text)
# 获取实际请求的URL
print(r.url)
# requests自动检测编码,可以使用encoding属性查看
print(r.encoding)
# 无论响应是文本还是二进制内容,我们都可以用content属性获得bytes对象
print(type(r.content))
# 获取响应头
print(r.headers)
print(r.headers['Content-Type'])
# requests对Cookie做了特殊处理,使得我们不必解析Cookie就可以轻松获取指定的Cookie:
print(r.cookies['ts'])
# 要在请求中传入Cookie,只需准备一个dict传入cookies参数:
cs = {'token': '12345', 'status': 'working'}
r = requests.get(url, cookies=cs)

# 控制请求的超时时间
# 2.5秒后超时
r = requests.get(url, timeout=2.5)
GET请求带参数
1
2
3
import requests

r = requests.get('https://www.douban.com/search', params={'q': 'python', 'cat': '1001'})
响应数据格式Json
1
2
3
4
5
import requests

# 指定请求响应数据的格式为json
r = requests.get('https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20%3D%202151330&format=json')
print(r.json())
请求头添加header
1
2
3
4
import requests

# 请求添加header
r = requests.get('https://www.douban.com/', headers={'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit'})
POST请求

POST请求和GET请求用法类似,只需要把get()方法变成post(),然后传入data参数作为POST请求的数据;

请求参数
  1. requests默认使用application/x-www-form-urlencoded对POST数据编码。
1
2
3
4
5
6
7
8
import requests

r = requests.post(
'https://accounts.douban.com/login',
data={
'form_email': 'abc@example.com',
'form_password': '123456'
})
  1. 如果要传递JSON数据,可以直接传入json关键字参数,内部自动序列化为JSON;
1
2
3
4
5
6
import requests

url = 'https://accounts.douban.com/login'
params = {'key': 'value'}
# json格式的参数(内部自动序列化为JSON)
r = requests.post(url, json=params)
  1. 上传文件:上传文件需要更复杂的编码格式,但是requests把它简化成files参数;
1
2
3
4
5
6
import requests

url = 'xxx'
# 在读取文件时,注意务必使用'rb'即二进制模式读取,这样获取的bytes长度才是文件的长度
upload_files = {'file': open('report.xls', 'rb')}
r = requests.post(url, files=upload_files)

chardet(编码检测)

用它来检测编码类型;

使用chardet检测编码非常容易,chardet支持检测中文、日文、韩文等多种语言。详情参考官方API文档

安装
1
$ pip install chardet
常用编码检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import chardet

# 检测ascii字节编码
r = chardet.detect(b'Hello, world!')
print(r)
# {'encoding(编码)': 'ascii', 'confidence': 1.0(正确的概率是1.0(即100%)), 'language': ''}

# 检测GBK编码的中文
data = '离离原上草,一岁一枯荣'.encode('gbk')
r = chardet.detect(data)
print(r)

# 检测UTF-8编码
data = '离离原上草,一岁一枯荣'.encode('utf-8')
r = chardet.detect(data)
print(r)

# 检测日文编码
data = '最新の主要ニュース'.encode('euc-jp')
r = chardet.detect(data)
print(r)

psutil(系统信息)

在Python中获取系统信息可以使用subprocess模块,另一个更简单的方法是使用psutil这个第三方模块。

psutil还可以获取用户信息、Windows服务等很多有用的系统信息,具体请参考psutil的官网:https://github.com/giampaolo/psutil;

安装
1
$ pip install psutil
获取CPU信息
1
2
3
4
5
6
7
8
9
import psutil

print('CPU逻辑数量:', psutil.cpu_count())
print('CPU物理核心:', psutil.cpu_count(logical=False))
print('统计CPU的用户/系统/空闲时间:', psutil.cpu_times())

# 类似top命令的CPU使用率,每秒刷新一次,累计10次
for x in range(10):
print(psutil.cpu_percent(interval=1, percpu=True))
获取内存信息
1
2
3
4
5
6
7
import psutil

print('物理内存信息:', psutil.virtual_memory())
print('交换内存信息:', psutil.swap_memory())
# 物理内存信息: svmem(total=8461451264, available=2556604416, percent=69.8, used=5904846848, free=2556604416)
# 交换内存信息: sswap(total=13561724928, used=10279280640, free=3282444288, percent=75.8, sin=0, sout=0)
# 返回的是字节为单位的整数,可以看到,总内存大小是8589934592 = 8 GB,已用7201386496 = 6.7 GB,使用了66.6%。而交换区大小是1073741824 = 1 GB。
获取磁盘信息

可以通过psutil获取磁盘分区、磁盘使用率和磁盘IO信息:

1
2
3
4
5
import psutil

print('磁盘分区信息:', psutil.disk_partitions())
print('磁盘使用情况:', psutil.disk_usage('/'))
print('磁盘IO:', psutil.disk_io_counters())
获取网络信息

psutil可以获取网络接口和网络连接信息;

1
2
3
4
5
6
import psutil

print('获取网络读写字节/包的个数:', psutil.net_io_counters())
print('获取网络接口信息:', psutil.net_if_addrs())
print('获取网络接口状态:', psutil.net_if_stats())
print('网络连接信息:', psutil.net_connections())
获取进程信息
  • psutil让Python程序获取系统信息;
  • psutil还可以获取用户信息、Windows服务等很多有用的系统信息,具体请参考psutil的官网:https://github.com/giampaolo/psutil;
  • 常用操作方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import psutil

print('所有进程ID:', psutil.pids())
# 获取指定进程ID=6856,其实就是当前chrome的进程
p = psutil.Process(6856)
print('进程名称:', p.name())
print('进程工作目录:', p.cwd())
print('进程exe路径:', p.exe())
print('进程启动的命令行:', p.cmdline())
print('父进程ID:', p.ppid())
print('父进程:', p.parent())
print('子进程列表:', p.children())
print('进程状态:', p.status())
print('进程用户名:', p.username())
print('进程创建时间:', p.create_time())
print('进程使用的CPU时间:', p.cpu_times())
print('进程使用的内存:', p.memory_info())
print('进程打开的文件:', p.open_files())
print('进程相关网络连接:', p.connections())
print('进程的线程数量:', p.num_threads())
print('所有线程信息:', p.threads())
print('进程环境变量:', p.environ())
print('结束进程:', p.terminate())
# print('进程终端:', p.terminal())
  • Linux ps命令用于显示当前进程 (process) 的状态,psutil还提供了一个test()函数,可以模拟出ps命令的效果。
1
print(psutil.test())

独立的python运行环境

virtualenv为python应用提供了隔离的Python运行环境,解决了不同应用间使用相同第三方或者外部模块引起多版本的冲突问题。

需要注意的是,pycharm创建的项目默认安装了virtualenv,并且pycharm terminal默认会进入venv环境。

安装virtualenv

1
$ pip3 install virtualenv

venv环境

venv环境是python项目根目录下的venv文件夹,通过命令virtualenv —no-site-packages可以让venv文件夹变为一个不带任何第三方包的“干净”的Python运行环境;

  1. 创建项目;
  2. 创建并进入venv环境,创建好venv环境后,命令行前面会有(venv)前缀,如下:
1
2
3
4
5
6
D:\pytest> $ virtualenv --no-site-packages venv
Using base prefix '/usr/local/.../Python.framework/Versions/3.4'
New python executable in venv/bin/python3.4
Also creating executable in venv/bin/python
Installing setuptools, pip, wheel...done.
(venv) D:\pytest>

venv环境指定项目的python版本

1
$ virtualenv -p /usr/bin/python2.7 my_project
  1. 项目venv环境中安装第三方包:
1
$ pip install chardet
  1. 退出venv环境:
1
$ deactivate
  1. 进入venv环境:
1
2
3
4
//linux
$ source my_project/bin/activate
//windows
$ my_project\venv\Scripts\activate

图形界面

Python支持多种图形界面的第三方库,包括:

  • Tk(Python自带的库Tkinter是支持Tk的)
  • wxWidgets
  • Qt
  • GTK

Tkinter

Python内置的Tkinter可以满足基本的GUI程序的要求,如果是非常复杂的GUI程序,建议用操作系统原生支持的语言和库来编写.

  • Tk是一个图形库,支持多个操作系统,使用Tcl语言开发;
  • Tk会调用操作系统提供的本地GUI接口,完成最终的GUI。
  • Tkinter封装了访问Tk的接口,我们的代码只需要调用Tkinter提供的接口就可以了;
GUI API
Frame

Frame是所有Widget的父容器,Frame则是可以容纳其他Widget的Widget,所有的Widget组合起来就是一棵树。

GUI案例
显示文本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from tkinter import *

# 从Frame派生一个Application类,这是所有Widget的父容器:
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
# pack()方法把Widget加入到父容器中
self.pack()
self.createWidgets()

def createWidgets(self):
# 创建一个Label
self.helloLabel = Label(self, text='Hello, world!')
self.helloLabel.pack()
# 创建一个Button
self.quitButton = Button(self, text='Quit', command=self.quit)
self.quitButton.pack()

# 设置窗口在左上角和大小
width, height, padx, pady = 300, 300, 0, 0
Tk().geometry('%dx%d9+%d+%d' % (width, height, padx, pady))

# 实例化Application,并启动消息循环
app = Application()
# 设置窗口标题:
app.master.title('Hello World')
# 主消息循环进入GUI界面
app.mainloop()
输入和弹窗
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from tkinter import *
import tkinter.messagebox as messagebox

class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.createWidgets()

def createWidgets(self):
# 创建一个输入框
self.nameInput = Entry(self)
self.nameInput.pack()
# 创建一个按钮
self.alertButton = Button(self, text='Hello', command=self.hello)
self.alertButton.pack()

def hello(self):
name = self.nameInput.get() or 'world'
# 显示一个弹窗
messagebox.showinfo('Message', 'Hello, %s' % name)

# 设置窗口在左上角和大小
width, height, padx, pady = 300, 300, 0, 0
Tk().geometry('%dx%d9+%d+%d' % (width, height, padx, pady))

app = Application()
# 设置窗口标题:
app.master.title('Hello World')

# 主消息循环:
app.mainloop()

turtle

Python内置了turtle库,基本上100%复制了原始的Turtle Graphics(海龟绘图)的所有功能。

turtle可以执行一个绘制图形的过程,绘制出各种复杂的图形;

案例
绘制一个长方形
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 导入turtle包的所有内容:
from turtle import *

# 设置笔刷宽度:
width(4)

# 前进:
forward(200)
# 右转90度:
right(90)

# 笔刷颜色:
pencolor('red')
forward(100)
right(90)

pencolor('green')
forward(200)
right(90)

pencolor('blue')
forward(100)
right(90)

# 调用done()使得窗口等待被关闭,否则将立刻关闭窗口:
done()
循环绘制5个五角星
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from turtle import *

def drawStar(x, y):
pu()
goto(x, y)
pd()
# set heading: 0
seth(0)
for i in range(5):
fd(40)
rt(144)

for x in range(0, 250, 50):
drawStar(x, 0)

done()
绘制一棵分型树
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from turtle import *

# 设置色彩模式是RGB:
colormode(255)

lt(90)

lv = 14
l = 120
s = 45

width(lv)

# 初始化RGB颜色:
r = 0
g = 0
b = 0
pencolor(r, g, b)

penup()
bk(l)
pendown()
fd(l)

def draw_tree(l, level):
global r, g, b
# save the current pen width
w = width()

# narrow the pen width
width(w * 3.0 / 4.0)
# set color:
r = r + 1
g = g + 2
b = b + 3
pencolor(r % 200, g % 200, b % 200)

l = 3.0 / 4.0 * l

lt(s)
fd(l)

if level < lv:
draw_tree(l, level + 1)
bk(l)
rt(2 * s)
fd(l)

if level < lv:
draw_tree(l, level + 1)
bk(l)
lt(s)

# restore the previous pen width
width(w)

speed("fastest")

draw_tree(l, 4)

done()

网络编程

用Python进行网络编程,就是在Python程序本身这个进程内,连接别的服务器进程的通信端口进行通信。

TCP/IP

互联网的协议简称TCP/IP协议.

IP协议

IP协议负责把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块,然后通过IP包发送出去。由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。

IPv4

IP地址实际上是一个32位整数(称为IPv4),以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。

IPv6

IPv6地址实际上是一个128位整数,它是目前使用的IPv4的升级版,以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334.

TCP协议

TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。

许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。

一个TCP报文除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。

三次握手
  1. 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
  2. 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
  3. 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
TCP协议栈的弱点

TCP连接的资源消耗,其中包括:数据包信息、条件状态、序列号等。通过故意不完成建立连接所需要的三次握手过程,造成连接一方的资源耗尽。

端口

80端口是Web服务的标准端口。其他服务都有对应的标准端口号,例如SMTP服务是25端口,FTP服务是21端口,等等。端口号小于1024的是Internet标准服务的端口,端口号大于1024的,可以任意使用。

TCP编程

socket
  • Socket是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。
  • 大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。
  • 用TCP协议进行Socket编程在Python中十分简单,对于客户端,要主动连接服务器的IP和指定端口,对于服务器,要首先监听指定端口,然后,对每一个新的连接,创建一个线程或进程来处理。通常,服务器程序会无限运行下去。
  • 同一个端口,被一个Socket绑定了以后,就不能被别的Socket绑定了。
socket使用步骤示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 导入socket库:
import socket

# 1. 创建一个socket:
# 创建Socket时,AF_INET指定使用IPv4协议,如果要用更先进的IPv6,就指定为AF_INET6。
# SOCK_STREAM指定使用面向流的TCP协议
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 建立连接:
s.connect(('www.sina.com.cn', 80))

# 3. 发送数据:
s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')

# 4. 接收数据:
buffer = []
while True:
# 每次最多接收1k字节:
d = s.recv(1024)
if d:
buffer.append(d)
else:
# 中断链接
break
data = b''.join(buffer)

# 5. 关闭连接:
s.close()

# 6. 处理结果
# 把HTTP头打印出来,网页内容保存到文件
header, html = data.split(b'\r\n\r\n', 1)
print(header.decode('utf-8'))
# 把接收的数据写入文件:
with open('sina.html', 'wb') as f:
f.write(html)
客户端

客户端使用socket请求服务端并获得响应;

大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。

服务器

和客户端编程相比,服务器编程就要复杂一些。

服务端处理思路
  1. 服务器进程首先要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器就与该客户端建立Socket连接,随后的通信就靠这个Socket连接了。
  2. 服务器会打开固定端口(比如80)监听,每来一个客户端连接,就创建该Socket连接。由于服务器会有大量来自客户端的连接,所以,服务器要能够区分一个Socket连接是和哪个客户端绑定的。一个Socket依赖4项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。
  3. 但是服务器还需要同时响应多个客户端的请求,所以,每个连接都需要一个新的进程或者新的线程来处理,否则,服务器一次就只能服务一个客户端了。

绑定监听的地址和端口。服务器可能有多块网卡,可以绑定到某一块网卡的IP地址上,也可以用0.0.0.0绑定到所有的网络地址,还可以用127.0.0.1绑定到本机地址。127.0.0.1是一个特殊的IP地址,表示本机地址,如果绑定到这个地址,客户端必须同时在本机运行才能连接,也就是说,外部的计算机无法连接进来。

CS通信示例

创建一个服务端,用来接收到用户的链接,当接收到用户输入的信息时,都将接收的内容加上“我不明白”再发回去,用户输入exit时,客户端退出socket连接。

实现步骤如下:

  1. 服务端代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import socket
import threading
import time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定监听端口:
# 可以用0.0.0.0绑定到所有的网络地址
# s.bind(('0.0.0.0', 9999))
# 可以用127.0.0.1绑定到本机地址,此时客户端必须同时在本机运行才能连接
s.bind(('127.0.0.1', 9999))

# 开始监听端口(传入的参数指定等待连接的最大数量)
s.listen(5)
print('Waiting for connection...')


# 每个连接都必须创建新线程(或进程)来处理
# 单线程在处理连接的过程中,无法接受其他客户端的连接;
def tcplink(sock, addr):
print('Accept new connection from %s:%s...' % addr)
sock.send(b'Welcome!')
while True:
data = sock.recv(1024)
time.sleep(1)
if not data or data.decode('utf-8') == 'exit':
break
sock.send(('我不明白"%s"是什么!' % data.decode('utf-8')).encode('utf-8'))
sock.close()
print('Connection from %s:%s closed.' % addr)


# 服务器程序通过一个永久循环来接受来自客户端的连接,accept()会等待并返回一个客户端的连接
while True:
# 接受一个新连接:
sock, addr = s.accept()
# 创建新线程来处理TCP连接:
t = threading.Thread(target=tcplink, args=(sock, addr))
t.start()
  1. 客户端代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接:
s.connect(('127.0.0.1', 9999))
# 接收欢迎消息:
print(s.recv(1024).decode('utf-8'))

# 通信:接收用户输入信息进行通信
while True:
inputStr = input("请输入信息,按回车发送:")
if inputStr == 'exit':
s.send(b'exit')
s.close()
print('通信结束!')
break
else:
s.send(inputStr.encode('utf-8'))
print(s.recv(1024).decode('utf-8'))

UDP编程

TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据。相对TCP,UDP则是面向无连接的协议。

  • 使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。
  • 虽然用UDP传输数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。

UDP的使用与TCP类似,但是不需要建立连接。此外,客户端不需要调用connect建立连接和服务端不需要调用listen,并且服务器绑定UDP端口和TCP端口互不冲突,也就是说,UDP的9999端口与TCP的9999端口可以各自绑定。

示例

服务端和客户端使用UDP通信,服务端接收客户端信息,加上hello后,再返回给客户端;

  1. 服务端代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import socket

# SOCK_DGRAM指定了这个Socket的类型是UDP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定端口
# UDP不需要调用listen()方法,而是直接接收来自任何客户端的数据
s.bind(('127.0.0.1', 9999))

# 此处省略多线程的写法
print('Bind UDP on 9999...')
while True:
# 接收数据(recvfrom()方法返回数据和客户端的地址与端口)
data, addr = s.recvfrom(1024)
print('Received from %s:%s.' % addr)
# 直接调用sendto()就可以把数据用UDP发给客户端
s.sendto(b'Hello, %s!' % data, addr)
  1. 客户端代码:
1
2
3
4
5
6
7
8
9
10
11
import socket
import time

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in [b'Michael', b'Tracy', b'Sarah']:
# 发送数据:
s.sendto(data, ('127.0.0.1', 9999))
# 接收数据:
print(s.recv(1024).decode('utf-8'))
time.sleep(1)
s.close()

实现收发电子邮件

名词解释

  • MUA:Mail User Agent——邮件用户代理。
  • MTA:Mail Transfer Agent——邮件传输代理,就是那些Email服务提供商,比如网易、新浪等等。
  • MDA:Mail Delivery Agent——邮件投递代理。

收发邮件

流程
1
发件人 -> MUA -> MTA -> MTA -> 若干个MTA -> MDA <- MUA <- 收件人

要编写程序来发送和接收邮件,本质上就是:

  1. 编写MUA把邮件发到MTA;
  2. 编写MUA从MDA上收邮件;
协议

发邮件时,MUA和MTA使用的协议就是SMTP:Simple Mail Transfer Protocol,后面的MTA到另一个MTA也是用SMTP协议。

收邮件时,MUA和MDA使用的协议有两种:POP:Post Office Protocol,目前版本是3,俗称POP3;IMAP:Internet Message Access Protocol,目前版本是4,优点是不但能取邮件,还可以直接操作MDA上存储的邮件,比如从收件箱移到垃圾箱,等等。

SMTP服务器配置

邮件客户端软件在发邮件时,会让你先配置SMTP服务器,也就是你要发到哪个MTA上。假设你正在使用163的邮箱,你就不能直接发到新浪的MTA上,因为它只服务新浪的用户,所以,你得填163提供的SMTP服务器地址:smtp.163.com,为了证明你是163的用户,SMTP服务器还要求你填写邮箱地址和邮箱口令,这样,MUA才能正常地把Email通过SMTP协议发送到MTA。

类似的,从MDA收邮件时,MDA服务器也要求验证你的邮箱口令,确保不会有人冒充你收取你的邮件,所以,Outlook之类的邮件客户端会要求你填写POP3或IMAP服务器地址、邮箱地址和口令,这样,MUA才能顺利地通过POP或IMAP协议从MDA取到邮件。

特别注意:代理客户端收发邮件,需要本地配置邮件的登录信息,并且,目前大多数邮件服务商都需要手动打开SMTP发信和POP收信的功能,否则只允许在网页登录;

SMTP发送邮件

SMTP是发送邮件的协议,Python内置对SMTP的支持,可以发送纯文本邮件、HTML邮件以及带附件的邮件。

Python对SMTP支持有smtplib和email两个模块,email负责构造邮件,smtplib负责发送邮件。

发文本邮件
  1. 构造邮件内容:
1
2
3
4
5
6
from email.mime.text import MIMEText
# 构造MIMEText对象时传入的参数:
# 第一个参数就是邮件正文,
# 第二个参数是MIME的subtype,传入'plain'表示纯文本,最终的MIME就是'text/plain',
# 第二个参数utf-8编码保证多语言兼容性。
msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')
  1. 邮件主题、发件人、收件人信息并不是通过SMTP协议发给MTA,而是包含在发给MTA的文本中的,如果没有,可能无法发送;
  2. 主要代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from email.mime.text import MIMEText
import smtplib
from email.utils import parseaddr, formataddr
from email.header import Header


# 格式化一个邮件地址(真实姓名+邮件地址)
# (注意不能简单地传入name <addr@example.com>,如果包含中文,需要通过Header对象进行编码)
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))

########################################################
# 用户邮件服务信息配置 #
########################################################
# 通过SMTP发出去
# 输入Email地址和口令:
# from_addr = input('From: ')
from_addr = 'xxx@163.com'
# password = input('Password: ')
password = 'xxx'
# 输入收件人地址:
# msg['To']接收的是字符串而不是list,如果有多个邮件地址,用,分隔即可。
# to_addr = input('To: ')
to_addr = 'xxx@qq.com'
# 输入SMTP服务器地址:
# smtp_server = input('SMTP server: ')
smtp_server = 'smtp.163.com'
########################################################


########################################################
# 构造邮件内容 #
########################################################
# 发送内容为:纯文本
# 构造邮件文本对象
# msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')
# 发送内容为:HTML
msg = MIMEText('<html><body><h1>Hello</h1>' +
'<p>send by <a href="http://www.python.org">Python</a>'
'<img height="92px" src="https://www.baidu.com/img/superlogo_c4d7df0a003d3db9b65e9ef0fe6da1ec.png" width="272px" alt="Google" /></p>' +
'</body></html>', 'html', 'utf-8')


# 发件人
msg['From'] = _format_addr('发件人姓名 <%s>' % from_addr)
# 收件人姓名可能被邮件服务商替换成注册用户名
msg['To'] = _format_addr('收件人姓名 <%s>' % to_addr)
msg['Subject'] = Header('邮件标题', 'utf-8').encode()
########################################################


########################################################
# 发邮件服务 #
########################################################
# SMTP协议默认端口是25
server = smtplib.SMTP(smtp_server, 25)
# 打印出和SMTP服务器交互的所有信息
server.set_debuglevel(1)
# login()方法用来登录SMTP服务器
server.login(from_addr, password)
# 发邮件
# 由于可以一次发给多个人,所以传入一个list
# 邮件正文是一个str,as_string()把MIMEText对象变成str
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
########################################################
发送附件

带附件的邮件可以看做包含若干部分的邮件:文本和各个附件本身,所以,可以构造一个MIMEMultipart对象代表邮件本身,然后往里面加上一个MIMEText作为邮件正文,再继续往里面加上表示附件的MIMEBase附件对象即可;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import smtplib
from email.utils import parseaddr, formataddr
from email.header import Header


# 格式化一个邮件地址(真实姓名+邮件地址)
# (注意不能简单地传入name <addr@example.com>,如果包含中文,需要通过Header对象进行编码)
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))


########################################################
# 用户邮件服务信息配置 #
########################################################
# 通过SMTP发出去
# 输入Email地址和口令:
# from_addr = input('From: ')
from_addr = 'xxx@163.com'
# password = input('Password: ')
password = 'xxx'
# 输入收件人地址:
# msg['To']接收的是字符串而不是list,如果有多个邮件地址,用,分隔即可。
# to_addr = input('To: ')
to_addr = 'xxx@qq.com'
# 输入SMTP服务器地址:
# smtp_server = input('SMTP server: ')
smtp_server = 'smtp.163.com'
########################################################


########################################################
# 构造邮件内容 #
########################################################
# 邮件对象:
msg = MIMEMultipart()
# 添加发件人
msg['From'] = _format_addr('发件人姓名 <%s>' % from_addr)
# 添加收件人姓名可能被邮件服务商替换成注册用户名
msg['To'] = _format_addr('收件人姓名 <%s>' % to_addr)
# 添加邮件标题
msg['Subject'] = Header('邮件标题', 'utf-8').encode()

# 添加邮件正文是MIMEText:
msg.attach(MIMEText('send with file...', 'plain', 'utf-8'))

# 添加附件就是加上一个MIMEBase,从本地读取一个图片:
with open('./taylor_swift.jpg', 'rb') as f:
# 设置附件的MIME和文件名,这里是png类型:
mime = MIMEBase('image', 'jpg', filename='taylor_swift.png')
# 加上必要的头信息:
mime.add_header('Content-Disposition', 'attachment', filename='taylor_swift.png')
mime.add_header('Content-ID', '<0>')
mime.add_header('X-Attachment-Id', '0')
# 把附件的内容读进来:
mime.set_payload(f.read())
# 用Base64编码:
encoders.encode_base64(mime)
# 添加到MIMEMultipart:
msg.attach(mime)
########################################################


########################################################
# 发邮件服务 #
########################################################
# SMTP协议默认端口是25
server = smtplib.SMTP(smtp_server, 25)
# 打印出和SMTP服务器交互的所有信息
server.set_debuglevel(1)
# login()方法用来登录SMTP服务器
server.login(from_addr, password)
# 发邮件
# 由于可以一次发给多个人,所以传入一个list
# 邮件正文是一个str,as_string()把MIMEText对象变成str
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
########################################################
发送图片

邮件服务商一般自动屏蔽邮件正文的图片,如果将图片添加邮件html正文,需要先上传图片,然后按照顺序编号cid:x引用图片即可;

在上传附件的代码后添加如下代码:

1
2
3
msg.attach(MIMEText('<html><body><h1>Hello</h1>' +
'<p><img src="cid:0"></p>' +
'</body></html>', 'html', 'utf-8'))
同时支持HTML和Plain格式

如果我们发送HTML邮件,收件人通过浏览器或者Outlook之类的软件是可以正常浏览邮件内容的,但是,如果收件人使用的设备太古老,查看不了HTML邮件怎么办?

办法是在发送HTML的同时再附加一个纯文本,如果收件人无法查看HTML格式的邮件,就可以自动降级查看纯文本邮件。

利用MIMEMultipart就可以组合一个HTML和Plain,要注意指定subtype是alternative:

1
2
3
4
5
6
7
8
msg = MIMEMultipart('alternative')
msg['From'] = ...
msg['To'] = ...
msg['Subject'] = ...

msg.attach(MIMEText('hello', 'plain', 'utf-8'))
msg.attach(MIMEText('<html><body><h1>Hello</h1></body></html>', 'html', 'utf-8'))
# 正常发送msg对象...
加密SMTP

使用标准的25端口连接SMTP服务器时,使用的是明文传输,发送邮件的整个过程可能会被窃听。要更安全地发送邮件,可以加密SMTP会话,实际上就是先创建SSL安全连接,然后再使用SMTP协议发送邮件。

只需要在创建SMTP对象后,立刻调用starttls()方法,就创建了安全连接。

示例

某些邮件服务商,例如Gmail,提供的SMTP服务必须要加密传输。我们来看看如何通过Gmail提供的安全SMTP发送邮件。
必须知道,Gmail的SMTP端口是587,因此,修改代码如下:

1
2
3
4
5
6
7
smtp_server = 'smtp.gmail.com'
smtp_port = 587
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
# 剩下的代码和前面的一模一样:
server.set_debuglevel(1)
...
SMTP发件总结

使用Python的smtplib发送邮件十分简单,只要掌握了各种邮件类型的构造方法,正确设置好邮件头,就可以顺利发出。

构造一个邮件对象就是一个Messag对象,如果构造一个MIMEText对象,就表示一个文本邮件对象,如果构造一个MIMEImage对象,就表示一个作为附件的图片,要把多个对象组合起来,就用MIMEMultipart对象,而MIMEBase可以表示任何对象。它们的继承关系如下:

1
2
3
4
5
6
7
Message
+- MIMEBase
+- MIMEMultipart
+- MIMENonMultipart
+- MIMEMessage
+- MIMEText
+- MIMEImage

这种嵌套关系就可以构造出任意复杂的邮件。你可以通过email.mime文档查看它们所在的包以及详细的用法。

POP3收取邮件

收取邮件就是编写一个MUA作为客户端,从MDA把邮件获取到用户的电脑或者手机上。收取邮件最常用的协议是POP协议,目前版本号是3,俗称POP3。

Python内置一个poplib模块,实现了POP3协议,可以直接用来收邮件。

收邮件

注意到POP3协议收取的不是一个已经可以阅读的邮件本身,而是邮件的原始文本,这和SMTP协议很像,SMTP发送的也是经过编码后的一大段文本。

要把POP3收取的文本变成可以阅读的邮件,还需要用email模块提供的各种类来解析原始文本,变成可阅读的邮件对象。

收取邮件分两步:

  1. 用poplib把邮件的原始文本下载到本地;
  2. 用email解析原始文本,还原为邮件对象。

用Python的poplib模块收取邮件分两步:第一步是用POP3协议把邮件获取到本地,第二步是用email模块把原始邮件解析为Message对象,然后,用适当的形式把邮件内容展示给用户即可。

通过POP3获取邮件

POP3协议本身很简单,以下面的代码为例,我们来获取最新的一封邮件内容:

用POP3获取邮件其实很简单,要获取所有邮件,只需要循环使用retr()把每一封邮件内容拿到即可。真正麻烦的是把邮件的原始内容解析为可以阅读的邮件对象。

如下获取邮件的代码(包含删除邮件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import poplib
from email.parser import Parser

# 1.输入邮件地址, 口令和POP3服务器地址:
email = input('Email: ')
password = input('Password: ')
pop3_server = input('POP3 server: ')

# 2.连接到POP3服务器:
server = poplib.POP3(pop3_server)
# 可以打开或关闭调试信息:
server.set_debuglevel(1)
# 可选:打印POP3服务器的欢迎文字:
print(server.getwelcome().decode('utf-8'))

# 身份认证:
server.user(email)
server.pass_(password)

# 3.stat()返回邮件数量和占用空间:
print('Messages: 邮件数量:%s 占用空间: %s' % server.stat())
# list()返回所有邮件的编号,编号即索引从1开始:
resp, mails, octets = server.list()
# 可以查看返回的列表类似[b'1 82923', b'2 2184', ...],内容为邮件编号和占用空间;
print('邮件列表',mails)

# 获取最新一封邮件, 注意索引号从1开始:
index = len(mails)
resp, lines, octets = server.retr(index)

# lines存储了邮件的原始文本的每一行,
# 可以获得整个邮件的原始文本:
msg_content = b'\r\n'.join(lines).decode('utf-8')
# print('邮件原始内容为:', msg_content)
# 稍后解析出邮件:
msg = Parser().parsestr(msg_content)
# print('邮件原始内容为:', msg)

# 可以根据邮件索引号直接从服务器删除邮件:
# server.dele(index)
# 关闭连接:
server.quit()
解析邮件

解析邮件的过程和上一节构造邮件正好相反,因此,先导入必要的模块,只需要一行代码就可以把邮件内容解析为Message对象:

1
2
3
4
5
6
7
8
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr

import poplib

...
msg = Parser().parsestr(msg_content)

但是这个Message对象本身可能是一个MIMEMultipart对象,即包含嵌套的其他MIMEBase对象,嵌套可能还不止一层。
所以我们要递归地打印出邮件Message对象的层次结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 解码邮件内容
# 邮件的Subject或者Email中包含的名字都是经过编码后的str,要正常显示,就必须decode:
def decode_str(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value


# 文本邮件的内容也是str,还需要检测编码,否则,非UTF-8编码的邮件都无法正常显示:
def guess_charset(msg):
charset = msg.get_charset()
if charset is None:
content_type = msg.get('Content-Type', '').lower()
pos = content_type.find('charset=')
if pos >= 0:
charset = content_type[pos + 8:].strip()
return charset


# 打印出邮件Message对象的层次结构
# indent用于缩进显示:
def print_info(msg, indent=0):
if indent == 0:
print('------------打印邮件简介------------')
for header in ['From', 'To', 'Subject']:
value = msg.get(header, '')
if value:
if header == 'Subject':
value = decode_str(value)
else:
hdr, addr = parseaddr(value)
name = decode_str(hdr)
value = u'%s <%s>' % (name, addr)
print('%s%s: %s' % (' ' * indent, header, value))
print('------------打印邮件简介-end-----------')
if msg.is_multipart():
parts = msg.get_payload()
for n, part in enumerate(parts):
print('%spart %s' % (' ' * indent, n))
print('%s--------------------' % (' ' * indent))
print_info(part, indent + 1)
else:
content_type = msg.get_content_type()
if content_type == 'text/plain' or content_type == 'text/html':
content = msg.get_payload(decode=True)
charset = guess_charset(msg)
if charset:
content = content.decode(charset)
print('%sText: %s' % (' ' * indent, content + '...'))
else:
print('%sAttachment: %s' % (' ' * indent, content_type))

数据的存储

定义数据的存储格式,主要有以下方式:

  • 文本文件存储:每条数据一行,字段用逗号隔开;
  • json文本文件存储:数据以json的格式保存本地;
  • SQLite
  • MySQL

SQLite

SQLite是一种嵌入式数据库,它的数据库就是一个文件。由于SQLite本身是C写的,而且体积很小,所以,经常被集成到各种应用程序中,甚至在iOS和Android的App中都可以集成。

使用SQLite

Python就内置了SQLite3,所以,在Python中使用SQLite,不需要安装任何东西,直接使用。

  • 在Python中操作数据库时,要先导入数据库对应的驱动,然后,通过Connection对象和Cursor对象操作数据。
  • 要确保打开的Connection对象和Cursor对象都正确地被关闭,否则,资源就会泄露。
  • 使用try:…except:…finally:…确保关闭掉Connection对象和Cursor对象;
  • 必须提交事务,修改的操作才会生效;
  • SQLite的sql中的参数占位符是问号?;
  • python操作数据库查询表的结果是list,list的每一个元素是tuple;

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 导入SQLite驱动:
import sqlite3
# 连接到SQLite数据库
# 数据库文件是test.db
# 如果文件不存在,会自动在当前目录创建:
conn = sqlite3.connect('test_sqlite.db')
# 创建一个Cursor:
cursor = conn.cursor()

# 执行一条SQL语句,创建user表:
cursor.execute('create table user (id varchar(20) primary key, name varchar(20))')

# 继续执行一条SQL语句,插入一条记录:
cursor.execute("insert into user (id, name) values ('1', 'zhangsan')")
cursor.execute("insert into user (id, name) values ('2', 'lisi')")

# 通过rowcount获得插入的行数:
print(cursor.rowcount)


# 执行查询语句:
cursor.execute('select * from user where id=?', ('1',))
# 获得查询结果集:
values = cursor.fetchall()
print(values)

# 关闭Cursor:
cursor.close()

# 提交事务(必须提交事务,修改的操作才会生效)
conn.commit()

# 关闭Connection:
conn.close()

如果SQL语句带有参数,那么需要把参数按照位置传递给execute()方法,有几个?占位符就必须对应几个参数,例如:

1
cursor.execute('select * from user where name=? and pwd=?', ('abc', 'password'))

MySQL

安装MySQL驱动
1
2
3
4
5
6
7
8
//mysql官方驱动
// --allow-external允许外部地址的标签,只有打上该标签pip方可下载外部地址模块
pip install mysql-connector-python --allow-external mysql-connector-python
//如果加--allow-external失败,就去掉
pip install mysql-connector-python

//如果上面的命令安装失败,可以试试另一个驱动:
pip install mysql-connector
操作MySQL
  • 执行INSERT等操作后要调用commit()提交事务;
  • 操作MySQL同操作Sqlite相同,但是MySQL的SQL中的参数占位符是%s。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 1. 导入MySQL驱动:
import mysql.connector

# 2. 连接数据库
config = {
'host': 'localhost',
'user': 'root',
'password': '123456',
'database': 'test',
'port': 3306,
'charset': 'utf8'
}
conn = mysql.connector.connect(**config)

# 3. 初见cursor对象操作数据库
cursor = conn.cursor()
# 4. 创建user表:
cursor.execute('create table user (id varchar(20) primary key, name varchar(20))')
# 5. 插入一行记录,注意MySQL的占位符是%s:
cursor.execute('insert into user (id, name) values (%s, %s)', ['1', 'Michael'])
print(cursor.rowcount)

# 6. 提交事务(必须提交事务才能修改数据):
conn.commit()
cursor.close()
# 运行查询:
cursor = conn.cursor()
cursor.execute('select * from user where id = %s', ('1',))
values = cursor.fetchall()
print(values)

# 关闭Cursor和Connection:
cursor.close()
conn.close()

ORM

ORM即Object-Relational Mapping,把关系数据库的表结构映射到对象上。ORM框架的作用就是把数据库表的一行记录与一个对象互相做自动转换。

SQLAlchemy

在Python中,最有名的ORM框架是SQLAlchemy。

安装
1
$ pip install sqlalchemy
使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 1. 导入:
from sqlalchemy import Column, String, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

# 2. 创建对象的基类:
Base = declarative_base()


# 3. 定义User对象:
class User(Base):
# 表的名字:
__tablename__ = 'user'

# 表的结构:
id = Column(String(20), primary_key=True)
name = Column(String(20))


# 4. 初始化数据库连接: create_engine('数据库类型+数据库驱动名称://用户名:口令@机器地址:端口号/数据库名')
engine = create_engine('mysql+mysqlconnector://root:123456@localhost:3306/test')
# 5. 创建DBSession类型(DBSession对象可视为当前数据库连接):
DBSession = sessionmaker(bind=engine)

# 6. ORM对数据库进行操作
# 6.1 利用ORM:向数据库表中添加一行记录
# 创建session对象:
session = DBSession()
# 创建新User对象:
new_user = User(id='5', name='Bob')
# 添加到session:
session.add(new_user)
# 提交即保存到数据库:
session.commit()
# 关闭session:
session.close()

# 6.2 利用ORM:向数据库表查询一行记录
# 创建Session:
session = DBSession()
# 创建Query查询,filter是where条件,最后调用one()返回唯一行,如果调用all()则返回所有行:
user = session.query(User).filter(User.id == '5').one()
# 打印类型和对象的name属性:
print('type:', type(user))
print('name:', user.name)
# 关闭Session:
session.close()

web开发

HTTP状态码

响应代码:

  • 200表示成功;
  • 3xx表示重定向;
  • 4xx表示客户端发送的请求有错误;
  • 5xx表示服务器端处理时发生了错误;

HTTP格式

每个HTTP请求和响应都遵循相同的格式,一个HTTP包含Header和Body两部分,其中Body是可选的。

Body:

  • Body的数据类型由Content-Type头来确定,如果是网页,Body就是文本,如果是图片,Body就是图片的二进制数据。
  • 当存在Content-Encoding时,Body数据是被压缩的,最常见的压缩方式是gzip,所以,看到Content-Encoding: gzip时,需要将Body数据先解压缩,才能得到真正的数据。压缩的目的在于减少Body的大小,加快网络传输。

WSGI

WSGI(Web Server Gateway Interface),WSGI接口定义非常简单,它只要求Web开发者实现一个函数,WSGI的处理函数,针对每个HTTP请求进行响应;

Python内置了一个WSGI服务器,这个模块叫wsgiref,它是用纯Python编写的WSGI服务器的参考实现。所谓“参考实现”是指该实现完全符合WSGI标准,但是不考虑任何运行效率,仅供开发和测试使用

  • 无论多么复杂的Web应用程序,入口都是一个WSGI处理函数。HTTP请求的所有输入信息都可以通过environ获得,HTTP响应的输出都可以通过start_response()加上函数返回值作为Body。
  • 复杂的Web应用程序,光靠一个WSGI函数来处理还是太底层了,我们需要在WSGI之上再抽象出Web框架,进一步简化Web开发。
http请求信息
1
2
3
4
//请求方式:GET/POST/PUT/DELETE
method = environ['REQUEST_METHOD']
//请求的地址,返回一个list
path = environ['PATH_INFO']
根据请求响应

如果通过判断请求信息来响应结果,项目将难以维护,此时就需要web框架了;

1
2
3
4
5
6
7
8
def application(environ, start_response):
method = environ['REQUEST_METHOD']
path = environ['PATH_INFO']
if method=='GET' and path=='/':
return handle_home(environ, start_response)
if method=='POST' and path='/signin':
return handle_signin(environ, start_response)
...
示例

如下,hello.py文件中定义用来响应客户端请求的函数,server.py利用wsgiref定义一个服务监听请求,响应application函数返回的结果;

1
2
3
4
5
6
# hello.py

def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
body = '<h1>Hello, %s!</h1>' % (environ['PATH_INFO'][1:] or 'web')
return [body.encode('utf-8')]
1
2
3
4
5
6
7
8
9
10
11
12
# server.py

# 从wsgiref模块导入:
from wsgiref.simple_server import make_server
# 导入我们自己编写的application函数:
from hello import application

# 创建一个服务器,IP地址为空,端口是8000,处理函数是application:
httpd = make_server('', 8000, application)
print('Serving HTTP on port 8000...')
# 开始监听HTTP请求:
httpd.serve_forever()

web框架

  • Flask: 轻量级 Web 应用框架(hot);
  • Django:全能型Web框架;
  • web.py:一个小巧的Web框架;
  • Bottle:和Flask类似的Web框架;
  • Tornado:Facebook的开源异步Web框架。

Flask

Flask 是Python比较流行的Web框架;

Flask是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2。Flask没有默认使用的数据库、窗体验证工具。用 extension增加其他功能。

然而,Flask保留了扩增的弹性,可以用Flask-extension加入这些功能:ORM、窗体验证工具、文件上传、各种开放式身份验证技术。此文章时的最新版本为1.0.2。

安装Flask

1
pip install -U Flask

WebAPI

核心概念
  • Flask通过Python的装饰器在内部自动地把URL和处理函数给关联起来;
  • Flask自带的Server在端口5000上监听;
  • Flask通过request.form[‘name’]来获取表单的内容。
示例

Flask框架,编写接口来支持GET/POST请求返回html;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from flask import Flask
from flask import request

app = Flask(__name__)


@app.route('/', methods=['GET', 'POST'])
def home():
return '<h1>Home</h1>'


@app.route('/signin', methods=['GET'])
def signin_form():
return '''<form action="/signin" method="post">
<p><input name="username"></p>
<p><input name="password" type="password"></p>
<p><button type="submit">Sign In</button></p>
</form>'''


@app.route('/signin', methods=['POST'])
def signin():
# 需要从request对象读取表单内容:
if request.form['username'] == 'admin' and request.form['password'] == 'password':
return '<h3>Hello, admin!</h3>'
return '<h3>Bad username or password.</h3>'


if __name__ == '__main__':
app.run()

jinja2模板技术

模板技术,我们以MVC模式进行开发:

  1. Controller层:Controller负责业务逻辑,即处理请求URL的函数;
  2. View层:HTML文档作为view层的UI模板,嵌入了一些变量 和指令,根据我们动态传入的数据,替换后,得到最终的HTML,发送给用户;
  3. Model层:是用来传给View的数据,通常dict作为Model;

通过MVC,我们在Python代码中处理M:Model和C:Controller,而V:View是通过模板处理的,这样,我们就成功地把Python代码和HTML代码最大限度地分离了。

jinja2
  • Flask通过render_template()函数来实现模板的渲染,具体html中支持模板渲染需要是jinja2。
  • 和Web框架类似,Python的模板也有很多种。Flask默认支持的模板是jinja2,所以我们先直接安装jinja2:
1
pip install jinja2
jinja2-API

在Jinja2模板中:

  • 模板文件:一定要把html模板放到templates目录下,templates和server.py服务文件在同级目录下;
1
2
3
4
5
project
|- server.py
|- templates
|- home.html
|- signin.html
  • 变量:用表示一个需要替换的变量。
  • 语法指令:很多时候,还需要循环、条件判断等指令语句,在Jinja2中,用
1
{% ... %}

表示指令,逻辑判断的语法就写在指令中。

1
2
3
4
<!--比如循环输出的代码-->
{% for i in page_list %}
<a href="/page/{{ i }}">{{ i }}</a>
{% endfor %}
示例

使用mvc模式简单的实现一个用户登录的功能!

1.编写模板(templates):

  • 首页模板home.html:
1
2
3
4
5
6
7
8
9
<!--用来显示首页的模板-->
<html>
<head>
<title>Home</title>
</head>
<body>
<h1 style="font-style:italic">Home</h1>
</body>
</html>
  • 登录页面模板from.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--用来显示登录表单的模板-->
<html>
<head>
<title>Please Sign In</title>
</head>
<body>
{% if message %}
<p style="color:red">{{ message }}</p>
{% endif %}
<form action="/signin" method="post">
<legend>Please sign in:</legend>
<p><input name="username" placeholder="Username" value="{{ username }}"></p>
<p><input name="password" placeholder="Password" type="password"></p>
<p>
<button type="submit">Sign In</button>
</p>
</form>
</body>
</html>
  • 登录成功页面模板signin-ok.html:
1
2
3
4
5
6
7
8
9
<!--登录成功的模板:-->
<html>
<head>
<title>Welcome, {{ username }}</title>
</head>
<body>
<p>Welcome, {{ username }}!</p>
</body>
</html>

2.编写Controller/Model层:server.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from flask import Flask, request, render_template

app = Flask(__name__)


@app.route('/', methods=['GET', 'POST'])
def home():
return render_template('home.html')


@app.route('/signin', methods=['GET'])
def signin_form():
return render_template('form.html')


@app.route('/signin', methods=['POST'])
def signin():
username = request.form['username']
password = request.form['password']
if username == 'admin' and password == 'password':
return render_template('signin-ok.html', username=username)
return render_template('form.html', message='Bad username or password', username=username)


if __name__ == '__main__':
app.run()

其它模板框架

除了Jinja2,常见的模板还有:

1
2
3
- Mako:用<% ... %>和${xxx}的一个模板;
- Cheetah:也是用<% ... %>和${xxx}的一个模板;
- Django:Django是一站式框架,内置一个用{% ... %}和{{ xxx }}的模板。

异步IO

在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。

解决IO阻塞的方法:

  1. 多线程;
  2. 异步IO;

多线程解决IO阻塞

解决方式

因为一个IO操作就阻塞了当前线程,导致其他代码无法执行,使用多线程或者多进程来并发执行代码,为多个用户服务。每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的线程不受影响。

性能问题

多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降。

由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的一种方法。

异步IO解决IO阻塞

解决IO阻塞问题的另一种解决方法是异步IO。

在“发出IO请求”到收到“IO完成”的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。

协程

协程,又称微线程,纤程。英文名coroutine(协同程序)。即多个程序系统完成工作的过程;

概念

子程序

子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,子程序调用总是一个入口,一次返回,调用顺序是明确的。一个线程就是执行一个子程序。

协程和子程序

而协程的调用和子程序不同。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

特点优势
  1. 协程可以看成子程序的切换,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,因此协程有着极高的执行效率。和多线程比,线程数量越多,协程的性能优势就越明显。
  2. 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
  3. 协程是一个线程执行,利用多核CPU最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
协程的实现

Python对协程的支持是通过generator实现的。

在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。Python的yield不但可以返回一个值,它还可以接收调用者发出的参数

示例解释协程

传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import time

def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
time.sleep(1)
r = '200 OK'

def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()

c = consumer()
produce(c)

注意到consumer函数是一个generator,把一个consumer传入produce后:

  1. 首先调用c.send(None)启动生成器;
  2. 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
  3. consumer通过yield拿到消息,处理,又通过yield把结果传回;
  4. produce拿到consumer处理的结果,继续生产下一条消息;
  5. produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

asyncio

asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持,asyncio提供了完善的异步IO支持;

asyncio可以实现单线程并发IO操作.

asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。

asyncio使用步骤

用asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from异步调用另一个coroutine实现异步操作。

  1. @asyncio.coroutine把一个generator函数逻辑标记为coroutine类型,处理函数的内部异步操作需要在coroutine中通过yield from完成;然后,我们就把这个coroutine扔到EventLoop中执行。
  2. yield from语法可以方便地异步调用另一个耗时逻辑的coroutine;
  3. coroutine类型函数的多次调用,可以封装成一组Task然后并发执行。
  4. EventLoop等待asyncio.wait(tasks)并发执行多个任务;
  5. 关闭EventLoop;
asyncio并发示例

示例1:实现并发执行两个耗时逻辑!

1
2
3
4
5
6
7
8
9
10
11
12
13
import threading
import asyncio

@asyncio.coroutine
def hello():
print('Hello world! (%s)' % threading.currentThread())
yield from asyncio.sleep(1)
print('Hello again! (%s)' % threading.currentThread())

loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

两个coroutine是由同一个线程并发执行的。asyncio.sleep()换成真正的IO操作,则多个coroutine就可以由一个线程并发执行。

示例2:用asyncio的异步网络连接来获取sina、sohu和163的网站首页!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import asyncio

@asyncio.coroutine
def wget(host):
print('wget %s...' % host)
connect = asyncio.open_connection(host, 80)
reader, writer = yield from connect
header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
writer.write(header.encode('utf-8'))
# writer.drain()执行写操作
yield from writer.drain()
while True:
line = yield from reader.readline()
if line == b'\r\n':
break
print('%s header > %s' % (host, line.decode('utf-8').rstrip()))
# Ignore the body, close the socket
writer.close()

loop = asyncio.get_event_loop()
# 3个连接由一个线程通过coroutine并发完成。
tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

async/await

新异步IO语法;

为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法async和await,可以让异步执行coroutine的代码更简洁易读。

使用

请注意,async和await是针对coroutine的新语法,要使用新的语法,只需要对asyncio做两步简单的替换:

  1. 把@asyncio.coroutine替换为async;
  2. 把yield from替换为await。
示例代码

并发执行异步任务打印hello!

1
2
3
4
5
6
7
8
9
10
11
import asyncio

async def hello():
print("Hello world!")
r = await asyncio.sleep(1)
print("Hello again!")

loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

aiohttp

服务器端实现单线程+coroutine并发IO操作;

asyncio可以实现单线程并发IO操作。如果仅用在客户端,发挥的威力不大。如果把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程+coroutine实现多用户的高并发支持。

asyncio实现了TCP、UDP、SSL等协议,aiohttp则是基于asyncio实现的HTTP框架。

安装aiohttp
1
pip install aiohttp
使用

创建aiohttp的初始化函数init(),init()也是一个coroutine,在函数中loop.create_server()则利用asyncio创建TCP服务。

示例

编写一个HTTP服务器,分别处理以下URL:

1
2
/ - 首页返回b'<h1>Index</h1>';
/hello/{name} - 根据URL参数返回文本hello, %s!。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import asyncio
from aiohttp import web

async def index(request):
await asyncio.sleep(0.5)
return web.Response(body=b'<h1>Index</h1>')


async def hello(request):
await asyncio.sleep(0.5)
text = '<h1>hello, %s!</h1>' % request.match_info['name']
return web.Response(body=text.encode('utf-8'))

app = web.Application()
app.router.add_routes([
web.get('/', index),
web.get('/hello/{name}', hello)
])
print('Server started at http://127.0.0.1:8080...')
web.run_app(app)

或者用loop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import asyncio

from aiohttp import web

async def index(request):
await asyncio.sleep(0.5)
return web.Response(body=b'<h1>Index</h1>')

async def hello(request):
await asyncio.sleep(0.5)
text = '<h1>hello, %s!</h1>' % request.match_info['name']
return web.Response(body=text.encode('utf-8'))

async def init(loop):
app = web.Application(loop=loop)
app.router.add_route('GET', '/', index)
app.router.add_route('GET', '/hello/{name}', hello)
srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000)
print('Server started at http://127.0.0.1:8000...')
return srv

loop = asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()
坚持原创技术分享,您的支持将鼓励我继续创作!